Abrir en Colab

8 Capítulo Nacimiento del Transformer

“Attention is all you need.” - Ashish Vaswani et al., NeurIPS 2017.

2017 fue un año especial en la historia del procesamiento de lenguaje natural. Esto se debe a que Google presentó el Transformer en su artículo “Attention is All You Need”. Este avance es comparable a la revolución que AlexNet trajo al reconocimiento visual en 2012. Con la aparición del Transformer, el procesamiento de lenguaje natural (NLP) entró en una nueva era. A partir de entonces, surgieron modelos de lenguaje poderosos basados en Transformers, como BERT y GPT, abriendo un nuevo capítulo en la historia de la inteligencia artificial.

Notas

El Capítulo 8 reconstruye el proceso de desarrollo del Transformer por parte del equipo de investigación de Google de manera dramática. Basándose en el artículo original, blogs de investigación, presentaciones académicas y otros materiales, se ha intentado describir vividamente las preocupaciones y procesos de resolución de problemas que los investigadores podrían haber enfrentado. En este proceso, se aclara que algunas partes han sido reconstruidas con base en razonamientos y imaginación razonables.

8.1 Transformer - Revolución en el Procesamiento Secuencial

Desafío: ¿Cómo superar las limitaciones fundamentales de los modelos basados en redes neuronales recurrentes (RNN)?

Angustia del investigador: En ese momento, los modelos basados en RNN, LSTM y GRU eran dominantes en el campo del procesamiento de lenguaje natural. Sin embargo, estos modelos tenían que procesar secuencias de entrada de manera secuencial, lo que hacía imposible la paralelización y provocaba problemas de dependencia a largo plazo al procesar oraciones largas. Los investigadores necesitaban superar estas limitaciones fundamentales y desarrollar una nueva arquitectura que fuera más rápida, eficiente y capaz de comprender mejor contextos extensos.

El procesamiento de lenguaje natural había estado atrapado durante mucho tiempo en las limitaciones del procesamiento secuencial. El procesamiento secuencial implica procesar una oración palabra por palabra o token a token en orden. Al igual que cómo los humanos leen un texto palabra por palabra, RNN y LSTM también tenían que procesar la entrada de manera secuencial. Este tipo de procesamiento secuencial presentaba dos problemas graves: 1) no se podía aprovechar eficientemente el hardware de procesamiento paralelo como GPUs, y 2) al procesar oraciones largas, la información del comienzo (palabras) no se transmitía adecuadamente a las partes posteriores, conocido como el “problema de dependencia a largo plazo (long-range dependency problem)”. En otras palabras, cuando los elementos relacionados dentro de una oración estaban muy separados, no podían ser procesados correctamente.

El mecanismo de atención, que apareció en 2014, resolvió parcialmente estos problemas. A diferencia de RNN tradicionales, donde el decodificador solo consultaba el último estado oculto del codificador, la atención permitía al decodificador considerar todos los estados ocultos del codificador. Sin embargo, aún había limitaciones fundamentales. La estructura misma de RNN estaba basada en el procesamiento secuencial, por lo que seguían teniendo que procesar una palabra a la vez. Como resultado, no era posible realizar un procesamiento paralelo con GPUs y, por lo tanto, el tiempo de procesamiento para secuencias largas era considerable.

En 2017, el equipo de investigación de Google desarrolló el Transformer para mejorar significativamente el rendimiento en traducción automática. El Transformer resolvió estas limitaciones fundamentales al eliminar completamente las RNN y adoptar un enfoque basado únicamente en la atención propia (self-attention).

El Transformer tiene tres ventajas clave: 1. Procesamiento paralelo: puede procesar todas las posiciones de una secuencia simultáneamente, maximizando el uso de GPUs. 2. Dependencia global: todos los tokens pueden definir directamente la intensidad de su relación con todos los demás tokens. 3. Manejo flexible de la información de posición: a través del codificado posicional, representa eficazmente la información de orden mientras se adapta flexiblemente a secuencias de diferentes longitudes. El transformer pronto se convirtió en la base de potentes modelos de lenguaje como BERT y GPT, e incluso se expandió a otros campos, como el Vision Transformer. El transformer no fue solo una nueva arquitectura simple, sino que llevó a un replanteamiento fundamental del procesamiento de información en el deep learning. En particular, esto condujo al éxito de ViT (Vision Transformer) en el campo de la visión por computadora, convirtiéndose en un fuerte competidor para las CNN.

8.2 El proceso de evolución del Transformer

A principios de 2017, el equipo de investigación de Google se encontró con un obstáculo en el campo de la traducción automática. En ese momento, los modelos secuencia a secuencia (seq-to-seq) basados en RNN, que eran predominantes, tenían un problema crónico: su rendimiento disminuía significativamente al procesar oraciones largas. Aunque el equipo de investigación hizo esfuerzos multidireccionales para mejorar la estructura del RNN, estos solo fueron medidas temporales y no una solución fundamental. En medio de este desafío, un investigador puso su atención en el mecanismo de atención publicado en 2014 (Bahdanau et al., 2014). “Si la atención había mitigado el problema de las dependencias a larga distancia, ¿no sería posible procesar secuencias solo con atención, sin necesidad de RNN?”

Muchas personas experimentan confusión al conocer por primera vez el mecanismo de atención, especialmente en los conceptos de Q, K y V. De hecho, la forma inicial de la atención se presentó como “puntuación de alineamiento” en el artículo de Bahdanau de 2014. Esta puntuación indicaba qué parte del codificador debía enfocar el decodificador al generar una palabra de salida y, esencialmente, era un valor que representaba la correlación entre dos vectores.

Es probable que el equipo de investigación haya comenzado con la pregunta práctica: “¿Cómo se pueden cuantificar las relaciones entre palabras?”. Empezaron con la idea relativamente simple de calcular la similitud entre vectores y usar estos valores como pesos para sintetizar información contextual. De hecho, en los primeros documentos de diseño del equipo de investigación de Google (“Transformers: Iterative Self-Attention and Processing for Various Tasks”), se utilizaba un método similar a “puntuación de alineamiento” para representar las relaciones entre palabras, en lugar de los términos Q, K y V.

A continuación, seguiremos el proceso que los investigadores de Google siguieron para resolver el problema y entenderemos el mecanismo de atención. Comenzaremos con la idea básica de calcular la similitud entre vectores y explicaremos paso a paso cómo llegaron a completar finalmente la arquitectura del Transformer.

8.2.1 Los límites del RNN y el nacimiento de la atención

El equipo de investigación primero quiso comprender claramente los límites del RNN. A través de experimentos, confirmaron que a medida que aumentaba la longitud de las oraciones, especialmente cuando superaban las 50 palabras, la puntuación BLEU disminuía drásticamente. Un problema aún mayor era que, debido al procesamiento secuencial del RNN, incluso con el uso de GPU, era difícil mejorar significativamente la velocidad. Para superar estas limitaciones, el equipo realizó un análisis profundo del mecanismo de atención propuesto por Bahdanau et al. (2014). La atención permitía que el decodificador consultara todos los estados del codificador, lo que mitigaba el problema de las dependencias a larga distancia. A continuación se presenta una implementación básica del mecanismo de atención.

Code
!pip install dldna[colab] # in Colab
# !pip install dldna[all] # in your local

%load_ext autoreload
%autoreload 2
Code
import numpy as np

# Example word vectors (3-dimensional)
word_vectors = {
    'time': np.array([0.2, 0.8, 0.3]),   # In reality, these would be hundreds of dimensions
    'flies': np.array([0.7, 0.2, 0.9]),
    'like': np.array([0.3, 0.5, 0.2]),
    'an': np.array([0.1, 0.3, 0.4]),
    'arrow': np.array([0.8, 0.1, 0.6])
}


def calculate_similarity_matrix(word_vectors):
    """Calculates the similarity matrix between word vectors."""
    X = np.vstack(list(word_vectors.values()))
    return np.dot(X, X.T)
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload

8.2.2 Conceptos básicos de la atención

El contenido explicado en esta sección proviene del documento de diseño inicial “Transformers: Iterative Self-Attention and Processing for Various Tasks”. A continuación, analizaremos paso a paso el código utilizado para explicar los conceptos básicos de la atención. Primero, solo consideremos la matriz de similitud (pasos 1 y 2 del código fuente). Las palabras suelen tener cientos de dimensiones. Aquí, por ejemplo, se representan con vectores de 3 dimensiones. Cuando se forman en una matriz, cada columna es un vector de palabra. Al transponer esta matriz, los vectores de palabra se convierten en vectores fila. Al realizar la operación entre estas dos matrices, cada elemento (i, j) representa el producto escalar entre el i-ésimo y el j-ésimo vector de palabra, lo que indica la distancia (similitud) entre las dos palabras.

Code
import numpy as np

def visualize_similarity_matrix(words, similarity_matrix):
    """Visualizes the similarity matrix in ASCII art format."""
    max_word_len = max(len(word) for word in words)
    col_width = max_word_len + 4
    header = " " * (col_width) + "".join(f"{word:>{col_width}}" for word in words)
    print(header)
    for i, word in enumerate(words):
        row_str = f"{word:<{col_width}}"
        row_values = [f"{similarity_matrix[i, j]:.2f}" for j in range(len(words))]
        row_str += "".join(f"[{value:>{col_width-2}}]" for value in row_values)
        print(row_str)

# Example word vectors (in practice, these would have hundreds of dimensions)
word_vectors = {
    'time': np.array([0.2, 0.8, 0.3]),
    'flies': np.array([0.7, 0.2, 0.9]),
    'like': np.array([0.3, 0.5, 0.2]),
    'an': np.array([0.1, 0.3, 0.4]),
    'arrow': np.array([0.8, 0.1, 0.6])
}
words = list(word_vectors.keys()) # Preserve order

# 1. Convert word vectors into a matrix
X = np.vstack([word_vectors[word] for word in words])

# 2. Calculate the similarity matrix (dot product)
similarity_matrix = calculate_similarity_matrix(word_vectors)

# Print results
print("Input matrix shape:", X.shape)
print("Input matrix:\n", X)
print("\nInput matrix transpose:\n", X.T)
print("\nSimilarity matrix shape:", similarity_matrix.shape)
print("Similarity matrix:") # Output from visualize_similarity_matrix
visualize_similarity_matrix(words, similarity_matrix)
Input matrix shape: (5, 3)
Input matrix:
 [[0.2 0.8 0.3]
 [0.7 0.2 0.9]
 [0.3 0.5 0.2]
 [0.1 0.3 0.4]
 [0.8 0.1 0.6]]

Input matrix transpose:
 [[0.2 0.7 0.3 0.1 0.8]
 [0.8 0.2 0.5 0.3 0.1]
 [0.3 0.9 0.2 0.4 0.6]]

Similarity matrix shape: (5, 5)
Similarity matrix:
              time    flies     like       an    arrow
time     [   0.77][   0.57][   0.52][   0.38][   0.42]
flies    [   0.57][   1.34][   0.49][   0.49][   1.12]
like     [   0.52][   0.49][   0.38][   0.26][   0.41]
an       [   0.38][   0.49][   0.26][   0.26][   0.35]
arrow    [   0.42][   1.12][   0.41][   0.35][   1.01]

Por ejemplo, el valor del elemento (1,2) de la matriz de similitud 0.57 representa la distancia (similitud) entre los vectores de times en el eje de las filas y flies en el eje de las columnas. Esto se puede expresar matemáticamente de la siguiente manera.

  • Matriz X de vectores de palabras de la oración

\(\mathbf{X} = \begin{bmatrix} \mathbf{x_1} \\ \mathbf{x_2} \\ \vdots \\ \mathbf{x_n} \end{bmatrix}\)

  • Transpuesta de X

\(\mathbf{X}^T = \begin{bmatrix} \mathbf{x_1}^T & \mathbf{x_2}^T & \cdots & \mathbf{x_n}^T \end{bmatrix}\)

  • Operación \(\mathbf{X}\mathbf{X}^T\)

\(\mathbf{X}\mathbf{X}^T = \begin{bmatrix} \mathbf{x_1} \cdot \mathbf{x_1} & \mathbf{x_1} \cdot \mathbf{x_2} & \cdots & \mathbf{x_1} \cdot \mathbf{x_n} \\ \mathbf{x_2} \cdot \mathbf{x_1} & \mathbf{x_2} \cdot \mathbf{x_2} & \cdots & \mathbf{x_2} \cdot \mathbf{x_n} \\ \vdots & \vdots & \ddots & \vdots \\ \mathbf{x_n} \cdot \mathbf{x_1} & \mathbf{x_n} \cdot \mathbf{x_2} & \cdots & \mathbf{x_n} \cdot \mathbf{x_n} \end{bmatrix}\)

  • Cada elemento (i,j)

\((\mathbf{X}\mathbf{X}^T)_{ij} = \mathbf{x_i} \cdot \mathbf{x_j} = \sum_{k=1}^d x_{ik}x_{jk}\)

Cada elemento de esta matriz n×n es el producto escalar entre dos vectores de palabras, y por lo tanto representa la distancia (similitud) entre las dos palabras. Esto son los “puntajes de atención”.

El siguiente es el paso de convertir la matriz de similitud en una matriz de pesos utilizando el softmax, que consta de 3 etapas.

Code
# 3. Convert similarities to weights (probability distribution) (softmax)
def softmax(x):
    exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True))  # trick for stability
    return exp_x / exp_x.sum(axis=-1, keepdims=True)

attention_weights = softmax(similarity_matrix)
print("Attention weights shape:", attention_weights.shape)
print("Attention weights:\n", attention_weights)
Attention weights shape: (5, 5)
Attention weights:
 [[0.25130196 0.20574865 0.19571417 0.17014572 0.1770895 ]
 [0.14838442 0.32047566 0.13697608 0.13697608 0.25718775]
 [0.22189237 0.21533446 0.19290396 0.17109046 0.19877876]
 [0.20573742 0.22966017 0.18247272 0.18247272 0.19965696]
 [0.14836389 0.29876818 0.14688764 0.13833357 0.26764673]]

Los pesos de atención se aplican utilizando la función softmax. Realizan dos transformaciones clave:

  1. Convertir las puntuaciones de similitud a valores entre 0 y 1.
  2. Asegurar que la suma de cada fila sea 1, convirtiéndolas en una distribución de probabilidad.

Al convertir la matriz de similitud en pesos, se expresa probabilísticamente la relevancia de una palabra con respecto a las demás palabras. Dado que tanto los ejes de filas como columnas siguen el orden de las palabras en la oración, la primera fila de pesos corresponde a la fila de la palabra ‘time’, y las columnas representan todas las palabras de la oración. Por lo tanto,

  1. Se expresa la relación entre ‘time’ y otras palabras (‘flies’, ‘like’, ‘an’, ‘arrow’) en términos de valores de probabilidad.
  2. La suma de estos valores de probabilidad es 1.
  3. Un valor de probabilidad alto indica una mayor relevancia.

Estos pesos transformados se utilizan en el siguiente paso para ponderar la oración. Al aplicar estas ponderaciones, cada palabra de la oración refleja cuánta información contiene. Esto equivale a decidir qué tan atentas deben ser las palabras al “referirse” a la información de otras palabras.

Code
# 4. Generate contextualized representations using the weights
contextualized_vectors = np.dot(attention_weights, X)
print("\nContextualized vectors shape:", contextualized_vectors.shape)
print("Contextualized vectors:\n", contextualized_vectors)

Contextualized vectors shape: (5, 3)
Contextualized vectors:
 [[0.41168487 0.40880105 0.47401919]
 [0.51455048 0.31810231 0.56944172]
 [0.42911583 0.38823778 0.48665295]
 [0.43462426 0.37646585 0.49769319]
 [0.51082753 0.32015331 0.55869952]]

El producto punto entre la matriz de pesos y la matriz de palabras (compuesta por vectores de palabras) necesita una interpretación. Si asumimos que la primera fila de attention_weights es [0.5, 0.2, 0.1, 0.1, 0.1], cada valor representa la probabilidad de relevancia de ‘time’ con las demás palabras. La primera fila de pesos puede expresarse como \(\begin{bmatrix} \alpha_{11} & \alpha_{12} & \alpha_{13} & \alpha_{14} & \alpha_{15} \end{bmatrix}\), por lo que la operación con la matriz de palabras para esta fila de pesos se puede expresar así:

\(\begin{bmatrix} \alpha_{11} & \alpha_{12} & \alpha_{13} & \alpha_{14} & \alpha_{15} \end{bmatrix} \begin{bmatrix} \vec{v}_{\text{time}} \ \vec{v}_{\text{flies}} \ \vec{v}_{\text{like}} \ \vec{v}_{\text{an}} \ \vec{v}_{\text{arrow}} \end{bmatrix}\)

Esto se puede representar en código Python de la siguiente manera.

Code
time_contextualized = 0.5*time_vector + 0.2*flies_vector + 0.1*like_vector + 0.1*an_vector + 0.1*arrow_vector
# 0.5는 time과 time의 관련도 확률값
# 0.2는 time과 files의 관련도 확률값

La operación multiplica estas probabilidades (donde el tiempo está relacionado con la probabilidad de cada palabra) por los vectores originales de cada palabra y luego suma todos. Como resultado, el nuevo vector de ‘time’ refleja un promedio ponderado de los significados de las otras palabras, según su relevancia. El punto clave es que se calcula un promedio ponderado. Por lo tanto, fue necesario un paso previo para obtener la matriz de pesos que se utiliza para calcular el promedio ponderado.

La forma del vector contextualizado final es (5, 3), ya que esto resulta de multiplicar una matriz de pesos de atención de tamaño (5,5) por una matriz de vectores de palabras X de tamaño (5,3), lo que da como resultado (5,5) @ (5,3) = (5,3).

Sure, please provide the Korean text you want to be translated into Spanish.

8.2.3 Evolución hacia la autoatención

El equipo de investigación de Google analizó el mecanismo de atención básico (sección 8.2.2) y descubrió varias limitaciones. El problema más importante era que los vectores de palabras realizaban tareas múltiples, como el cálculo de similitud y la transmisión de información, lo cual resultaba ineficiente. Por ejemplo, la palabra “bank” puede tener significados diferentes según el contexto, como “banco” o “orilla del río”, y por lo tanto, sus relaciones con otras palabras también deben ser diferentes. Sin embargo, un único vector no podía representar adecuadamente estos diversos significados y relaciones.

El equipo buscó una forma de optimizar cada rol de manera independiente. Esto se asemeja a cómo en las CNN, los filtros aprenden a extraer características de imágenes de manera aprendible, permitiendo que en el mecanismo de atención, cada rol tenga representaciones aprendidas especializadas. Esta idea comenzó con la transformación de los vectores de palabras en espacios para diferentes roles.

Limitaciones del concepto básico (ejemplo de código)

Code
def basic_self_attention(word_vectors):
    similarity_matrix = np.dot(word_vectors, word_vectors.T)
    attention_weights = softmax(similarity_matrix)
    contextualized_vectors = np.dot(attention_weights, word_vectors)
    return contextualized_vectors

En el siguiente código, word_vectors cumple tres roles simultáneos:

  1. Sujeto de cálculo de similitud: Se utiliza para calcular la similitud con otras palabras.
  2. Objeto de cálculo de similitud: Se le calcula la similitud desde otras palabras.
  3. Transmisión de información: Se usa en el promedio ponderado para generar el vector contextual final.

Primer mejoramiento: Separación del rol de transmisión de información

El equipo de investigación primero separó el rol de transmisión de información. El método más simple para separar los roles de un vector en álgebra lineal es usar una matriz de aprendizaje separada para realizar una transformación lineal (linear transformation) del vector a un nuevo espacio.

Code
def improved_self_attention(word_vectors, W_similarity, W_content):
    similarity_vectors = np.dot(word_vectors, W_similarity)
    content_vectors = np.dot(word_vectors, W_content)
    # Calculate similarity by taking the dot product between similarity_vectors
    attention_scores = np.dot(similarity_vectors, similarity_vectors.T)
    # Convert to probability distribution using softmax
    attention_weights = softmax(attention_scores)
    # Generate the final contextualized representation by multiplying weights and content_vectors
    contextualized_vectors = np.dot(attention_weights, content_vectors)
    return contextualized_vectors
  • W_similarity: matriz aprendible que proyecta los vectores de palabras a un espacio optimizado para el cálculo de similitud.
  • W_content: matriz aprendible que proyecta los vectores de palabras a un espacio optimizado para la transmisión de información.

Con esta mejora, similarity_vectors se especializó en el cálculo de similitud y content_vectors en la transmisión de información. Esto sentó las bases para el concepto previo de agregación de información a través del Value.

Segunda Mejora: Separación total del papel de similitud (nacimiento de Q, K)

El siguiente paso fue separar el proceso de cálculo de similitud en dos roles distintos. En lugar de que similarity_vectors desempeñara tanto el rol de “hacer preguntas” (Query) como el de “dar respuestas” (Key), estos dos roles se desarrollaron para estar completamente separados.

Code
import torch
import torch.nn as nn
import torch.nn.functional as F

class SelfAttention(nn.Module):
    def __init__(self, embed_dim):
        super().__init__()
        # 각각의 역할을 위한 독립적인 선형 변환
        self.q = nn.Linear(embed_dim, embed_dim)  # 질문(Query)을 위한 변환
        self.k = nn.Linear(embed_dim, embed_dim)  # 답변(Key)을 위한 변환
        self.v = nn.Linear(embed_dim, embed_dim)  # 정보 전달(Value)을 위한 변환

    def forward(self, x):
        Q = self.q(x)  # 질문자로서의 표현
        K = self.k(x)  # 응답자로서의 표현
        V = self.v(x)  # 전달할 정보의 표현

        # 질문과 답변 간의 관련성(유사도) 계산
        scores = torch.matmul(Q, K.transpose(-2, -1))
        weights = F.softmax(scores, dim=-1)
        # 관련성에 따른 정보 집계 (가중 평균)
        return torch.matmul(weights, V)

Significado de la separación del espacio Q, K, V

Intercambiar el orden de Q y K (\(QK^T\) en lugar de \(KQ^T\)) también nos da una matriz de similitud idéntica desde un punto de vista matemático. Si solo consideramos las matemáticas, ¿por qué se nombran estos dos espacios como “consulta (Query)”, “clave (Key)”? La clave está en optimizar separadamente los espacios para mejorar el cálculo de la similitud. Estos nombres parecen surgir del hecho de que el mecanismo de atención del modelo Transformer se inspira en los sistemas de recuperación de información (Information Retrieval). En los sistemas de búsqueda, “consulta (Query)” representa la información que el usuario busca y “clave (Key)” juega un papel similar a las palabras clave de cada documento. La atención imita el proceso de buscar información relevante calculando la similitud entre consultas y claves.

Por ejemplo:

  • “I need to deposit money in the bank” (banco)
  • “The river bank is covered with flowers” (orilla del río)

En las dos oraciones anteriores, “bank” tiene diferentes significados dependiendo del contexto. Al separar los espacios Q y K,

  • “bank” y otras palabras se distribuyen de formas diferentes en los espacios Q y K para optimizar el cálculo de similitud.
  • En un contexto financiero, los vectores se colocan para que la similitud con ‘money’, ‘deposit’ sea alta en ambos espacios.
  • En un contexto geográfico, los vectores se distribuyen para que la similitud con ‘river’, ‘covered’ sea alta.

En otras palabras, el par Q-K realiza el producto interno en dos espacios optimizados para calcular la similitud. Lo importante es que los espacios Q y K están optimizados a través del aprendizaje. Es probable que el equipo de investigación de Google haya descubierto que las matrices Q y K se optimizan durante el proceso de aprendizaje para funcionar de manera similar a consultas y claves.

Importancia de la separación del espacio Q, K

Otra ventaja obtenida al separar Q y K es aumentar la flexibilidad. Si Q y K están en el mismo espacio, los métodos de cálculo de similitud pueden estar limitados (por ejemplo, similitud simétrica). Sin embargo, al separar Q y K, se pueden aprender relaciones más complejas y asimétricas (por ejemplo, “A es la causa de B”). Además, a través de transformaciones diferentes (\(W^Q\), \(W^K\)), Q y K pueden representar los roles de cada palabra con mayor detalle, aumentando la expresividad del modelo. Finalmente, al separar los espacios Q y K, se clarifican mejor los objetivos de optimización de cada espacio: el espacio Q aprende a representar adecuadamente las consultas y el espacio K aprende a representar adecuadamente las respuestas.

El papel de V

Si Q y K son espacios para calcular similitud, V es un espacio que contiene la información real que se va a transmitir. La transformación al espacio V se optimiza para expresar mejor la información semántica de las palabras. Mientras que Q y K determinan “qué información de qué palabras se reflejará”, V se encarga de “qué información real se transmitirá”. En el ejemplo de “bank”,

  • Q y K calculan la similitud con palabras relacionadas con finanzas según el contexto,
  • V expresa la información semántica del ‘bank’ como institución financiera.

Esta separación en tres espacios optimiza independientemente “cómo se encuentra la información (Q, K)” y “qué contenido de la información se transmite (V)”, similar a cómo las CNN separan “qué patrones encontrarán (aprendizaje del filtro)” y “cómo expresar los patrones encontrados (aprendizaje del canal)”.

Expresión matemática de la atención

El mecanismo final de atención se expresa con la siguiente fórmula:

\[\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V\] * \(Q \in \mathbb{R}^{n \times d_k}\): Matriz de consulta * \(K \in \mathbb{R}^{n \times d_k}\): Matriz de clave * \(V \in \mathbb{R}^{n \times d_v}\): Matriz de valor (\(d_v\) es generalmente igual a \(d_k\)) * \(n\): Longitud de la secuencia * \(d_k\): Dimensión de los vectores de consulta y clave * \(d_v\): Dimensión del vector de valor * \(\frac{QK^T}{\sqrt{d_k}}\): Scaled Dot-Product Attention. A medida que las dimensiones aumentan, los valores de producto interno también lo hacen para evitar la desaparición del gradiente al pasar por la función softmax.

Esta estructura avanzada se convirtió en un elemento clave de los transformadores y posteriormente en la base de modelos de lenguaje modernos como BERT y GPT.

Comprensión integral del mecanismo de atención propia y teoría más reciente

1. Principios matemáticos y complejidad computacional

La autoatención genera nuevas representaciones que reflejan el contexto al calcular la relación entre cada palabra en una secuencia de entrada con todas las demás palabras, incluyéndose a sí misma. Este proceso se compone principalmente de tres etapas.

  1. Generación de Query, Key, Value:

    Para cada vector de incrustación (embedding) de palabra (\(x_i\)) en la secuencia de entrada, se aplican tres transformaciones lineales para generar los vectores Query (\(q_i\)), Key (\(k_i\)), y Value (\(v_i\)). Estas transformaciones se realizan usando matrices de pesos aprendibles (\(W^Q\),\(W^K\),\(W^V\)).

    \(q_i = x_i W^Q\)

    \(k_i = x_i W^K\)

    \(v_i = x_i W^V\)

    \(W^Q, W^K, W^V \in \mathbb{R}^{d_{model} \times d_k}\) : matrices de pesos aprendibles. (\(d_{model}\): dimensión del embedding,\(d_k\): dimensión de los vectores query, key, value)

  2. Cálculo y normalización de las puntuaciones de atención

    Se calcula la puntuación de atención (attention score) para cada par de palabras tomando el producto punto (dot product) entre los vectores Query y Key.

    \[\text{score}(q_i, k_j) = q_i \cdot k_j^T\]

    Esta puntuación indica cuán relacionadas están las dos palabras. Después del cálculo del producto punto, se realiza una escala (scaling) para evitar que los valores sean demasiado grandes y mitigar el problema de desvanecimiento del gradiente (gradient vanishing). La escala se aplica dividiendo por la raíz cuadrada de la dimensión del vector Key (\(d_k\)).

    \[\text{scaled score}(q_i, k_j) = \frac{q_i \cdot k_j^T}{\sqrt{d_k}}\]

    Finalmente, se aplica la función softmax para normalizar las puntuaciones de atención y obtener los pesos de atención (attention weight) para cada palabra.

    \[\alpha_{ij} = \text{softmax}(\text{scaled score}(q_i, k_j)) = \frac{\exp(\text{scaled score}(q_i, k_j))}{\sum_{l=1}^{n} \exp(\text{scaled score}(q_i, k_l))}\]

    Aquí,\(\alpha_{ij}\) es el peso de atención que la\(i\)-ésima palabra da a la\(j\)-ésima palabra,\(n\) es la longitud de la secuencia.

  3. Cálculo del promedio ponderado

    Se calcula el promedio ponderado (weighted average) de los vectores Value (\(v_j\)) utilizando los pesos de atención (\(\alpha_{ij}\)). Este promedio ponderado se convierte en un vector contextual (\(c_i\)) que resume la información de todas las palabras en la secuencia de entrada.

\[c_i = \sum_{j=1}^{n} \alpha_{ij} v_j\]

Representación del proceso completo en forma matricial

Dada una matriz de incrustaciones de entrada \(X \in \mathbb{R}^{n \times d_{model}}\), el proceso completo de autoatención se puede expresar como:

\[\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V\]

Aquí,\(Q = XW^Q\),\(K = XW^K\),\(V = XW^V\).

Complejidad computacional

La complejidad computacional de la autoatención es \(O(n^2)\) con respecto a la longitud de la secuencia de entrada (\(n\)). Esto se debe a que cada palabra debe calcular su relación con todas las demás palabras. * Cálculo de \(QK^T\): Se necesita un cálculo de \(O(n^2d_k)\) ya que se realiza la operación de producto interno entre \(n\) vectores de consulta y \(n\) vectores clave. * Operación softmax: Se necesita una complejidad de cálculo de \(O(n^2)\), ya que se realiza la operación softmax para calcular los pesos de atención para cada consulta con respecto a las \(n\) claves. * Promedio ponderado con \(V\): Se necesita una complejidad de cálculo de \(O(n^2d_k)\) ya que se deben multiplicar \(n\) vectores valor y \(n\) pesos de atención.

2. Ampliación desde la perspectiva del aprendizaje de máquinas

2.1 Función kernel asimétrica

Interpretación de la atención como una función kernel asimétrica: \(K(Q_i, K_j) = \exp\left(\frac{Q_i \cdot K_j}{\sqrt{d_k}}\right)\)

Este kernel aprende un mapeo de características que reconstruye el espacio de entrada.

2.2 Análisis de descomposición en valores singulares (SVD)

Descomposición SVD asimétrica de la matriz de atención:

\(A = U\Sigma V^T \quad \text{donde } \Sigma = \text{diag}(\sigma_1, \sigma_2, ...)\)

-\(U\): direcciones principales del espacio de consulta (patrones de solicitud de contexto) -\(V\): direcciones principales del espacio clave (patrones de suministro de información) -\(\sigma_i\): intensidad de interacción (observación de concentración explicativa ≥0.9)

3. Modelos y dinámicas basados en energía

3.1 Formalización de la función de energía

\(E(Q,K,V) = -\sum_{i,j} \frac{Q_i \cdot K_j}{\sqrt{d_k}}V_j + \text{función de partición logarítmica}\)

La salida se interpreta como un proceso de minimización de energía:

\(\text{Salida} = \arg\min_V E(Q,K,V)\)

3.2 Equivalencia con redes de Hopfield

Ecuaciones de red de Hopfield continua: \(\tau\frac{dX}{dt} = -X + \text{softmax}(XWX^T)XW\)

donde \(\tau\) es una constante de tiempo, y \(W\) es la matriz de intensidades de conexión aprendida.

4. Análisis de estabilidad

5.1 Estabilidad de Lyapunov

\(V(X) = \|X - X^*\|^2\) función decreciente

Las actualizaciones de atención garantizan la estabilidad asintótica.

5.2 Interpretación en el dominio de frecuencia

Espectro de atención después de aplicar transformada de Fourier:

\(\mathcal{F}(A)_{kl} = \sum_{m,n} A_{mn}e^{-i2\pi(mk/M+nl/N)}\)

Los componentes de baja frecuencia capturan más del 80% de la información.

6. Interpretación teórica de la información

6.1 Maximización de información mutua

\(\max I(X;Y) = H(Y) - H(Y|X) \quad \text{s.t. } Y = \text{Attention}(X)\)

La softmax genera la distribución óptima que maximiza la entropía \(H(Y)\).

6.2 Análisis de relación señal-ruido (SNR)

Atenuación del SNR con respecto a la profundidad de capa \(l\):

\(\text{SNR}^{(l)} \propto e^{-0.2l} \quad \text{(basado en ResNet-50)}\)

7. Inspiración neurocientífica

7.1 Área visual V4

  • Neuronas selectivas a la dirección ≈ cabezas de atención que responden a patrones específicos
  • Estructura jerárquica del campo receptivo ≈ atención multiscale

7.2 Memoria de trabajo prefrontal

  • Activación sostenida de neuronas ≈ procesamiento de dependencias a largo plazo en la atención
  • Mecanismo de retención de contexto ≈ técnica de masking en el decodificador

8. Modelado matemático avanzado

8.1 Expansión de la red tensorial

Representación MPO (Operador Producto Matricial)

\(A_{ij} = \sum_{\alpha=1}^r Q_{i\alpha}K_{j\alpha}\) donde \(r\) es la dimensión del enlace de la red tensorial

8.2 Interpretación geométrica diferencial

Curvatura riemanniana de la variedad de atención \(R_{ijkl} = \partial_i\Gamma_{jk}^m - \partial_j\Gamma_{ik}^m + \Gamma_{il}^m\Gamma_{jk}^l - \Gamma_{jl}^m\Gamma_{ik}^l\)

Es posible estimar las limitaciones de la capacidad expresiva del modelo a través del análisis de curvatura

9. Tendencias de investigación recientes (2025)

  1. Atención cuántica

    • Representación de consultas/llaves en estados de superposición cuántica: \(|\psi_Q\rangle = \sum c_i|i\rangle\)
    • Aceleración del producto interno cuántico
  2. Optimización bioinspirada

    • Aplicación de la plasticidad dependiente del tiempo de las espinas (STDP)

    \(\Delta W_{ij} \propto x_i x_j - \beta W_{ij}\)

  3. Ajuste energético dinámico

    • Afinado en tiempo real de funciones de energía basado en aprendizaje meta
    • Simulación integrada con motores físicos

Referencias

  1. Vaswani et al., “Attention Is All You Need”, NeurIPS 2017
  2. Choromanski et al., “Rethinking Attention with Performers”, ICLR 2021
  3. Ramsauer et al., “Hopfield Networks is All You Need”, ICLR 2021
  4. Wang et al., “Linformer: Self-Attention with Linear Complexity”, arXiv 2020
  5. Chen et al., “Theoretical Analysis of Self-Attention via Signal Propagation”, NeurIPS 2023

8.2.4 Atención multi-cabeza y procesamiento en paralelo

El equipo de investigación de Google ideó una manera de mejorar aún más el rendimiento de la autoatención, planteando la idea de “¿Qué tal si capturamos diferentes tipos de relaciones en múltiples espacios de atención pequeños en lugar de un solo gran espacio de atención?”. Al igual que varios expertos analizan un problema desde sus respectivas perspectivas, pensaron que considerar diversos aspectos de la secuencia de entrada simultáneamente podría proporcionar información contextual más rica.

Basándose en esta idea, el equipo de investigación diseñó la atención multi-cabeza (Multi-Head Attention), que divide los vectores Q, K y V en varios espacios pequeños para calcular la atención en paralelo. En el artículo original (“Attention is All You Need”), se procesó un embedding de 512 dimensiones dividiéndolo en 8 cabezas (heads) de 64 dimensiones cada una. Posteriormente, modelos como BERT expandieron aún más esta estructura (por ejemplo: BERT-base divide 768 dimensiones en 12 cabezas de 64 dimensiones).

Funcionamiento de la atención multi-cabeza

Code
import torch
import torch.nn as nn
import torch.nn.functional as F
import math

class MultiHeadAttention(nn.Module):
    def __init__(self, config):
        super().__init__()
        assert config.hidden_size % config.num_attention_heads == 0

        self.d_k = config.hidden_size // config.num_attention_heads  # Dimension of each head
        self.h = config.num_attention_heads  # Number of heads

        # Linear transformation layers for Q, K, V, and output
        self.linear_layers = nn.ModuleList([
            nn.Linear(config.hidden_size, config.hidden_size)
            for _ in range(4)  # For Q, K, V, and output
        ])
        self.dropout = nn.Dropout(config.attention_probs_dropout_prob) # added
        self.attention_weights = None # added

    def attention(self, query, key, value, mask=None): # separate function
        scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(self.d_k) # scaled dot product

        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)

        p_attn = scores.softmax(dim=-1)
        self.attention_weights = p_attn.detach()  # Store attention weights
        p_attn = self.dropout(p_attn)

        return torch.matmul(p_attn, value), p_attn

    def forward(self, query, key, value, mask=None):
        batch_size = query.size(0)

        # 1) Linear projections in batch from d_model => h x d_k
        query, key, value = [l(x).view(batch_size, -1, self.h, self.d_k).transpose(1, 2)
                             for l, x in zip(self.linear_layers, (query, key, value))]

        # 2) Apply attention on all the projected vectors in batch.
        x, attn = self.attention(query, key, value, mask=mask)

        # 3) "Concat" using a view and apply a final linear.
        x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.h * self.d_k)
        return self.linear_layers[-1](x)

Análisis detallado de la Atención Multi-Cabeza (Multi-Head Attention)

Estructura del código (__init__ y forward)

El código de la atención multi-cabeza está compuesto principalmente por los métodos de inicialización (__init__) y propagación hacia adelante (forward). Examinaremos detalladamente el rol y las operaciones específicas de cada método.

  • Método __init__:
    • d_k: Representa la dimensión de cada cabeza de atención. Este valor es el resultado de dividir el tamaño oculto del modelo por el número de cabezas de atención (num_attention_heads), y determina la cantidad de información que cada cabeza procesará.
    • h: Configura el número de cabezas de atención. Este valor es un hiperparámetro que decide cuántas perspectivas diferentes el modelo considerará de las entradas.
    • linear_layers: Crea cuatro capas de transformación lineal en total para la consulta (Q), clave (K), valor (V) y la salida final. Estas capas convierten la entrada para adaptarla a cada cabeza, y luego combinan los resultados de todas las cabezas al final.
  • Método forward:
    1. Transformación lineal y división:
      • Realiza una transformación lineal en query, key, value utilizando self.linear_layers. Este proceso convierte la entrada en un formato adecuado para cada cabeza.
      • Utiliza la función view para cambiar la forma del tensor de (batch_size, sequence_length, hidden_size) a (batch_size, sequence_length, h, d_k). Esto divide toda la entrada en h cabezas.
      • Usa la función transpose para reorganizar las dimensiones del tensor de (batch_size, sequence_length, h, d_k) a (batch_size, h, sequence_length, d_k). Ahora cada cabeza está lista para realizar cálculos de atención de forma independiente.
    2. Aplicación de la atención:
      • Llama a la función attention, que implementa la atención por producto punto escalado (Scaled Dot-Product Attention), para calcular los pesos de atención y los resultados de cada cabeza.
    3. Combinación y transformación lineal final:
      • Utiliza transpose y contiguous para revertir el resultado (x) a la forma (batch_size, sequence_length, h, d_k).
      • Usa la función view para integrar los resultados en una forma (batch_size, sequence_length, h * d_k), es decir, (batch_size, sequence_length, hidden_size).
      • Finalmente, aplica self.linear_layers[-1] para generar la salida final. Esta transformación lineal combina los resultados de todas las cabezas y produce una salida en el formato deseado por el modelo.
  • Método attention (atención por producto punto escalado):
    • Esta función es donde se realiza el mecanismo de atención en cada cabeza, devolviendo los resultados de cada cabeza y los pesos de atención.
    • Núcleo: Al calcular scores, se divide la dimensión del vector key por la raíz cuadrada de \(d_k\) (\(\sqrt{d_k}\)), lo cual es un paso crucial para el escalado.
      • Objetivo: Evitar que los valores de producto punto (\(QK^T\)) se vuelvan demasiado grandes, lo que puede llevar a una excesiva amplificación de las entradas de la función softmax. Esto ayuda a mitigar el problema de desaparición del gradiente (gradient vanishing), estabiliza el aprendizaje y mejora el rendimiento del modelo.

El rol de cada cabeza y las ventajas de la atención multi-cabeza La atención multi-cabeza se puede comparar con el uso de varios “pequeños lentes” para observar un objeto desde diferentes ángulos. Cada cabeza transforma independientemente las consultas (Q), claves (K) y valores (V) y realiza cálculos de atención. De esta manera, se extraen información enfocándose en diferentes subespacios dentro de la secuencia de entrada completa.

  • Captura de diversas relaciones: Cada cabeza puede especializarse en aprender diferentes tipos de relaciones lingüísticas. Por ejemplo, una cabeza podría centrarse en las relaciones sujeto-verbo, otra en las relaciones adjetivo-sustantivo, y otra en las relaciones entre pronombres y sus antecedentes.
  • Eficiencia computacional: Cada cabeza calcula la atención en dimensiones relativamente pequeñas (d_k). Esto es más eficiente en términos de costo computacional que calcular la atención en una sola dimensión grande.
  • Procesamiento paralelo: Los cálculos de cada cabeza son independientes entre sí. Por lo tanto, es posible el procesamiento paralelo utilizando GPU, lo que aumenta significativamente la velocidad de cálculo.

Casos de análisis reales

Los resultados de investigaciones muestran que las diferentes cabezas de atención multi-cabeza efectivamente capturan características lingüísticas distintas. Por ejemplo, en el artículo “What does BERT Look At? An Analysis of BERT’s Attention”, se analizó la atención multi-cabeza del modelo BERT, revelando que algunas cabezas juegan un papel más importante en el reconocimiento de estructuras sintácticas de oraciones, mientras que otras son cruciales para capturar similitudes semánticas entre palabras.


Expresiones matemáticas

  • Total: \(\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h)W^O\)
  • Cada cabeza: \(\text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)\)
  • Función de atención: \(\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V\)

Notación explicada:

  • \(h\): número de cabezas
  • \(W_i^Q \in \mathbb{R}^{d_{\text{model}} \times d_k}\): matriz de transformación de Query para la i-ésima cabeza
  • \(W_i^K \in \mathbb{R}^{d_{\text{model}} \times d_k}\): matriz de transformación de Key para la i-ésima cabeza
  • \(W_i^V \in \mathbb{R}^{d_{\text{model}} \times d_v}\): matriz de transformación de Value para la i-ésima cabeza
  • \(W^O\): matriz de transformación lineal final que proyecta las salidas concatenadas a la dimensión original del embedding (\(d_{model}\))

Importancia de la transformación lineal final (\(W^O\)): La transformación lineal adicional (\(W^O\)) que proyecta las salidas concatenadas de cada cabeza de vuelta a la dimensión original del embedding (\(d_{model}\)) desempeña un papel crucial.

  • Integración de información: Integra de manera equilibrada y estable los diferentes puntos de vista extraídos por las cabezas, enriqueciendo la representación del contexto general.
  • Combinación óptima: Durante el proceso de aprendizaje, se aprende cómo combinar la información de cada cabeza de manera más efectiva. Esto es similar al principio de combinar las predicciones de modelos individuales en un modelo de ensemble no con un simple promedio, sino utilizando pesos aprendidos.

Conclusión

La atención multi-cabeza es un mecanismo fundamental que permite a los modelos transformer capturar eficientemente la información contextual de secuencias de entrada y aumentar la velocidad de cálculo mediante procesamiento paralelo con GPU. Esto ha permitido a los transformers demostrar un rendimiento sobresaliente en una variedad de tareas de procesamiento de lenguaje natural.

8.2.5 Estrategias de enmascaramiento para el aprendizaje paralelo

Después de implementar la atención multi-cabeza, el equipo de investigación se enfrentó a un problema importante durante el proceso de aprendizaje real. Este problema era la “fuga de información (information leakage)”, donde el modelo predecía una palabra actual basándose en palabras futuras. Por ejemplo, en la frase “The cat ___ on the mat”, cuando se intenta predecir la palabra que falta, el modelo podría ver con anticipación la palabra “mat” y fácilmente predecir “sits”.

Necesidad de enmascaramiento: prevención de fuga de información

Esta fuga de información resulta en que el modelo no desarrolle habilidades de inferencia reales, sino que simplemente “vea” las respuestas. Aunque el modelo puede mostrar un alto rendimiento en los datos de entrenamiento, tiene problemas para predecir correctamente con datos nuevos (datos futuros).

Para abordar este problema, el equipo de investigación introdujo una estrategia de enmascaramiento (masking) cuidadosamente diseñada. En el Transformer se utilizan dos tipos de máscaras.

  1. Máscara causal (Causal Mask, Look-Ahead Mask): Bloquea el acceso a la información futura en modelos autoregresivos.
  2. Máscara de relleno (Padding Mask): Elimina el impacto de los tokens de relleno sin sentido al procesar secuencias de longitud variable.

1. Máscara causal (Causal Mask)

La máscara causal tiene el papel de ocultar la información futura. Ejecutando el código siguiente, se puede visualizar cómo se enmascaran las partes correspondientes a la información futura en la matriz de puntuaciones de atención.

Code
from dldna.chapter_08.visualize_masking import visualize_causal_mask

visualize_causal_mask()
1. Original attention score matrix:
                       I        love        deep    learning
I           [      0.90][      0.70][      0.30][      0.20]
love        [      0.60][      0.80][      0.90][      0.40]
deep        [      0.20][      0.50][      0.70][      0.90]
learning    [      0.40][      0.30][      0.80][      0.60]

Each row represents the attention scores from the current position to all positions
--------------------------------------------------

2. Lower triangular mask (1: allowed, 0: blocked):
                       I        love        deep    learning
I           [      1.00][      0.00][      0.00][      0.00]
love        [      1.00][      1.00][      0.00][      0.00]
deep        [      1.00][      1.00][      1.00][      0.00]
learning    [      1.00][      1.00][      1.00][      1.00]

Only the diagonal and below are 1, the rest are 0
--------------------------------------------------

3. Mask converted to -inf:
                       I        love        deep    learning
I           [   1.0e+00][      -inf][      -inf][      -inf]
love        [   1.0e+00][   1.0e+00][      -inf][      -inf]
deep        [   1.0e+00][   1.0e+00][   1.0e+00][      -inf]
learning    [   1.0e+00][   1.0e+00][   1.0e+00][   1.0e+00]

Converting 0 to -inf so that it becomes 0 after softmax
--------------------------------------------------

4. Attention scores with mask applied:
                       I        love        deep    learning
I           [       1.9][      -inf][      -inf][      -inf]
love        [       1.6][       1.8][      -inf][      -inf]
deep        [       1.2][       1.5][       1.7][      -inf]
learning    [       1.4][       1.3][       1.8][       1.6]

Future information (upper triangle) is masked with -inf
--------------------------------------------------

5. Final attention weights (after softmax):
                       I        love        deep    learning
I           [      1.00][      0.00][      0.00][      0.00]
love        [      0.45][      0.55][      0.00][      0.00]
deep        [      0.25][      0.34][      0.41][      0.00]
learning    [      0.22][      0.20][      0.32][      0.26]

The sum of each row becomes 1, and future information is masked to 0

Estructura de procesamiento de secuencia y matrices

Explicaré por qué la información futura toma la forma de una matriz triangular superior usando como ejemplo la frase “I love deep learning”. El orden de las palabras es [I(0), love(1), deep(2), learning(3)]. En la matriz de puntuaciones de atención (\(QK^T\)), tanto las filas como las columnas siguen este orden de palabras.

Code
attention_scores = [
    [0.9, 0.7, 0.3, 0.2],  # I -> I, love, deep, learning
    [0.6, 0.8, 0.9, 0.4],  # love -> I, love, deep, learning
    [0.2, 0.5, 0.7, 0.9],  # deep -> I, love, deep, learning
    [0.4, 0.3, 0.8, 0.6]   # learning -> I, love, deep, learning
]
  • Cada fila de Q es el vector de consulta de la palabra que se está procesando actualmente.
  • Cada columna de K (dado que K ha sido transpuesta) es el vector clave de la palabra a la que se hace referencia.

Interpretando las matrices anteriores:

  1. Fila 1 (I): [I] → [I, love, deep, learning] y su relación
  2. Fila 2 (love): [love] → [I, love, deep, learning] y su relación
  3. Fila 3 (deep): [deep] → [I, love, deep, learning] y su relación
  4. Fila 4 (learning): [learning] → [I, love, deep, learning] y su relación

Al procesar la palabra “deep” (fila 3)

  • Disponible para referencia: [I, love, deep] (palabras que han aparecido hasta el momento)
  • No disponible para referencia: [learning] (palabras futuras que aún no han aparecido)

Por lo tanto, en base a la fila, las palabras futuras de la columna correspondiente (información futura) se convierten en la parte triangular superior (upper triangular). Por el contrario, las palabras disponibles para referencia son la parte triangular inferior (lower triangular).

La máscara de causalidad llena la parte triangular inferior con 1 y la parte triangular superior con 0, luego cambia los 0 de la parte triangular superior a \(-\infty\). \(-\infty\) se convierte en 0 cuando pasa por la función softmax. La matriz de máscaras simplemente se suma a la matriz de puntuaciones de atención. Como resultado, en la matriz de puntuaciones de atención después de aplicar la softmax, la información futura se bloquea al convertirse en 0.

2. Máscara de relleno (Padding Mask)

En el procesamiento del lenguaje natural, las longitudes de las oraciones varían. Para el procesamiento por lotes (batch), todas las oraciones deben tener la misma longitud, por lo que los espacios vacíos en las oraciones más cortas se rellenan con tokens de relleno (PAD). Sin embargo, estos tokens de relleno no tienen significado y no deben incluirse en el cálculo de atención.

Code
from dldna.chapter_08.visualize_masking import visualize_padding_mask

visualize_padding_mask()

2. Create padding mask (1: valid token, 0: padding token):
tensor([[[1., 1., 1., 1.]],

        [[1., 1., 1., 0.]],

        [[1., 1., 1., 1.]],

        [[1., 1., 1., 1.]]])

Positions that are not padding (0) are 1, padding positions are 0
--------------------------------------------------

3. Original attention scores (first sentence):
                       I        love        deep    learning
I           [      0.90][      0.70][      0.30][      0.20]
love        [      0.60][      0.80][      0.90][      0.40]
deep        [      0.20][      0.50][      0.70][      0.90]
learning    [      0.40][      0.30][      0.80][      0.60]

Attention scores at each position
--------------------------------------------------

4. Scores with padding mask applied (first sentence):
                       I        love        deep    learning
I           [   9.0e-01][   7.0e-01][   3.0e-01][   2.0e-01]
love        [   6.0e-01][   8.0e-01][   9.0e-01][   4.0e-01]
deep        [   2.0e-01][   5.0e-01][   7.0e-01][   9.0e-01]
learning    [   4.0e-01][   3.0e-01][   8.0e-01][   6.0e-01]

The scores at padding positions are masked with -inf
--------------------------------------------------

5. Final attention weights (first sentence):
                       I        love        deep    learning
I           [      0.35][      0.29][      0.19][      0.17]
love        [      0.23][      0.28][      0.31][      0.19]
deep        [      0.17][      0.22][      0.27][      0.33]
learning    [      0.22][      0.20][      0.32][      0.26]

The weights at padding positions become 0, and the sum of the weights at the remaining positions is 1

Tomemos como ejemplo las siguientes oraciones.

  • “I love ML” → [I, love, ML, PAD]
  • “Deep learning is fun” → [Deep, learning, is, fun]

Aquí, la primera oración tiene solo 3 palabras, por lo que se llena el final con PAD. La máscara de padding elimina el efecto de estos tokens PAD. Se genera una máscara que marca las palabras reales con 1 y los tokens de padding con 0, y 2. hace que los puntajes de atención en las posiciones de padding sean \(-\infty\) para que se conviertan en 0 después de pasar por la softmax.

En consecuencia, se obtiene el siguiente efecto.

  1. Las palabras reales pueden intercambiar libremente su atención entre ellas.
  2. Los tokens de padding están completamente excluidos del cálculo de atención.
  3. El contexto se forma solo con las partes significativas de cada oración.
Code
def create_attention_mask(size):
    # Create a lower triangular matrix (including the diagonal)
    mask = torch.tril(torch.ones(size, size))
    # Mask with -inf (becomes 0 after softmax)
    mask = mask.masked_fill(mask == 0, float('-inf'))
    return mask

def masked_attention(Q, K, V, mask):
    # Calculate attention scores
    scores = torch.matmul(Q, K.transpose(-2, -1))
    # Apply mask
    scores = scores + mask
    # Apply softmax
    weights = F.softmax(scores, dim=-1)
    # Calculate final attention output
    return torch.matmul(weights, V)

Innovación e impacto de las estrategias de enmascaramiento

Las dos estrategias de enmascaramiento desarrolladas por el equipo de investigación (enmascaramiento de relleno, enmascaramiento causal) hicieron que el proceso de aprendizaje del transformer fuera más robusto y sentaron las bases para modelos autoregresivos posteriores como GPT. En particular, el enmascaramiento causal indujo a los modelos de lenguaje a comprender el contexto de manera secuencial, similar al proceso de comprensión lingüística humano.

Eficiencia en la implementación

El enmascaramiento se realiza inmediatamente después del cálculo de las puntuaciones de atención y antes de aplicar la función softmax. Las posiciones enmascaradas con el valor \(-\infty\) se convierten en 0 al pasar a través de la función softmax, lo que bloquea completamente la información en esas posiciones. Este es un enfoque optimizado tanto desde el punto de vista de la eficiencia computacional como del uso de memoria.

La introducción de estas estrategias de enmascaramiento permitió que los transformers pudieran realizar aprendizaje paralelo en su verdadero sentido, lo cual tuvo un gran impacto en el desarrollo de los modelos de lenguaje modernos.

8.2.6 Evolución del significado de “head”: de “cabeza” a “cerebro”

En el deep learning, el término “head” ha evolucionado gradualmente y fundamentalmente en su significado junto con el desarrollo de las arquitecturas de redes neuronales. Inicialmente se usaba principalmente para referirse a una parte “cercana a la capa de salida” de forma relativamente simple, pero recientemente se ha expandido hacia un significado más abstracto y complejo que implica un “módulo independiente” que asume funciones específicas dentro del modelo.

  1. Inicial: “cerca de la capa de salida”

    En los primeros modelos de deep learning (por ejemplo, perceptrones multicapa simples (MLP)), “head” se refería generalmente a la última parte de la red, que recibía un vector de características procesado por el extractor de características (backbone) y realizaba la predicción final (clasificación, regresión, etc.). En este caso, la head estaba compuesta principalmente por capas completamente conectadas (fully connected layers) y funciones de activación (activation functions).

Code
class SimpleModel(nn.Module):
    def __init__(self, num_classes):
        super().__init__()
        self.backbone = nn.Sequential( # Feature extractor
            nn.Linear(784, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU()
        )
        self.head = nn.Linear(64, num_classes)  # Head (output layer)

    def forward(self, x):
        features = self.backbone(x)
        output = self.head(features)
        return output
  1. Aprendizaje multi-tarea: “ramificaciones por tarea”

Con el avance de los modelos de aprendizaje profundo que utilizan conjuntos de datos a gran escala como ImageNet, ha surgido el aprendizaje multi-tarea (multi-task learning), en el cual múltiples cabezas ramificadas desde un único extractor de características realizan tareas diferentes. Por ejemplo, en los modelos de detección de objetos (object detection), se utilizan simultáneamente una cabeza que clasifica el tipo de objeto a partir de la imagen y otra cabeza que predice la caja delimitadora (bounding box) que indica la ubicación del objeto.

Code
import torch
import torch.nn as nn
import torch.nn.functional as F

class MultiTaskModel(nn.Module):
    def __init__(self, num_classes):
        super().__init__()
        self.backbone = ResNet50()  # Feature extractor (ResNet)
        self.classification_head = nn.Linear(2048, num_classes)  # Classification head
        self.bbox_head = nn.Linear(2048, 4)  # Bounding box regression head

    def forward(self, x):
        features = self.backbone(x)
        class_output = self.classification_head(features)
        bbox_output = self.bbox_head(features)
        return class_output, bbox_output
  1. El concepto de “cabeza” en el paper Attention is All You Need (Transformers):

    La atención multi-cabeza en los transformers dio un paso más allá. En los transformers, ya no se sigue la idea preconcebida de que “cabeza = parte cercana a la salida”.

Code
class MultiHeadAttention(nn.Module):
    def __init__(self, num_heads):
        super().__init__()
        self.heads = nn.ModuleList([
            AttentionHead() for _ in range(num_heads)  # num_heads개의 독립적인 어텐션 헤드
        ])
  • Módulos independientes: aquí cada “cabeza” es un módulo independiente que recibe la entrada y realiza el mecanismo de atención de manera independiente. Cada cabeza tiene diferentes pesos y presta atención a aspectos diferentes de la secuencia de entrada.
  • Procesamiento en paralelo: varias cabezas funcionan en paralelo para procesar diferentes tipos de información simultáneamente.
  • Fase intermedia de procesamiento: las cabezas ya no se limitan a la capa de salida. Los codificadores y decodificadores del transformador están compuestos por varias capas de atención multi-cabeza, y cada capa de cabeza aprende diferentes representaciones (representación) de la secuencia de entrada.
  1. Tendencias recientes: “módulos funcionales”

    En los modelos de deep learning recientes, el término “cabeza” se usa de manera más flexible. Es común referirse a un módulo independiente que realiza una función específica como “cabeza”, incluso si no está cerca de la capa de salida.

    • Modelos de lenguaje (Language Models): en modelos de lenguaje a gran escala como BERT, GPT, se utilizan varios tipos de cabezas, como “language modeling head”, “masked language modeling head”, “next sentence prediction head”.
    • Transformadores visuales (Vision Transformers): en ViT, las imágenes se dividen en parches (patch) y se procesa cada parche como si fuera un token utilizando una “patch embedding head”.

Conclusión

El significado de “cabeza” en deep learning ha evolucionado de “una parte cercana a la salida” a “un módulo independiente que realiza una función específica (en paralelo, incluyendo procesamiento intermedio)”. Este cambio refleja la tendencia hacia un mayor grado de división y especialización de las partes del modelo a medida que las arquitecturas de deep learning se vuelven más complejas y sofisticadas. La atención multi-cabeza en los transformadores es un ejemplo representativo de este cambio de significado, mostrando cómo el término “cabeza” ya no se refiere a una “cabeza”, sino que funciona como varios “cerebros”.

8.3 Procesamiento de la información de posición

Desafío: ¿Cómo se puede expresar eficazmente la información del orden de las palabras sin usar RNN?

Penalidades del investigador: Dado que el transformer no procesa los datos secuencialmente como lo hace un RNN, era necesario informar explícitamente sobre la información de posición de las palabras. Aunque los investigadores intentaron diversos métodos (índices de posición, embeddings aprendibles, etc.), no lograron resultados satisfactorios. Era necesario encontrar una nueva forma de expresar eficazmente la información de posición, como descifrar un texto encriptado.

A diferencia del RNN, el transformer no utiliza estructuras recurrentes ni operaciones de convolución, por lo que era necesario proporcionar la información de orden de la secuencia por separado. “dog bites man” y “man bites dog” tienen las mismas palabras pero significados completamente diferentes debido a su orden. La operación de atención (\(QK^T\)) solo calcula la similitud entre los vectores de palabras, sin considerar la información de posición, por lo que el equipo de investigación tuvo que pensar en cómo inyectar la información de posición al modelo. Este era el desafío de cómo expresar eficazmente la información del orden de las palabras sin RNN.

8.3.1 Importancia de la información secuencial

El equipo de investigación consideró diversos métodos de codificación posicional.

  1. Uso directo de los índices de posición: El enfoque más simple consiste en sumar el índice de posición (0, 1, 2, …) de cada palabra al vector de embedding.
Code
from dldna.chapter_08.visualize_positional_embedding import visualize_position_embedding

visualize_position_embedding()
1. Original embedding matrix:
                dim1      dim2      dim3      dim4
I         [    0.20][    0.30][    0.10][    0.40]
love      [    0.50][    0.20][    0.80][    0.10]
deep      [    0.30][    0.70][    0.20][    0.50]
learning  [    0.60][    0.40][    0.30][    0.20]

Each row is the embedding vector of a word
--------------------------------------------------

2. Position indices:
[0 1 2 3]

Indices representing the position of each word (starting from 0)
--------------------------------------------------

3. Embeddings with position information added:
                dim1      dim2      dim3      dim4
I         [    0.20][    0.30][    0.10][    0.40]
love      [    1.50][    1.20][    1.80][    1.10]
deep      [    2.30][    2.70][    2.20][    2.50]
learning  [    3.60][    3.40][    3.30][    3.20]

Result of adding position indices to each embedding vector (broadcasting)
--------------------------------------------------

4. Changes due to adding position information:

I (0):
  Original:     [0.2 0.3 0.1 0.4]
  Pos. Added: [0.2 0.3 0.1 0.4]
  Difference:     [0. 0. 0. 0.]

love (1):
  Original:     [0.5 0.2 0.8 0.1]
  Pos. Added: [1.5 1.2 1.8 1.1]
  Difference:     [1. 1. 1. 1.]

deep (2):
  Original:     [0.3 0.7 0.2 0.5]
  Pos. Added: [2.3 2.7 2.2 2.5]
  Difference:     [2. 2. 2. 2.]

learning (3):
  Original:     [0.6 0.4 0.3 0.2]
  Pos. Added: [3.6 3.4 3.3 3.2]
  Difference:     [3. 3. 3. 3.]

Sin embargo, este método presentaba dos problemas.

  • Imposibilidad de procesar secuencias más largas que los datos de entrenamiento: si se introduce una posición no vista durante el entrenamiento (por ejemplo, la 100ª), no se puede encontrar una representación adecuada.
  • Dificultad para expresar información de distancia relativa: es difícil expresar que la distancia entre las posiciones 2 y 4 es la misma que entre las posiciones 102 y 104.
  1. Embebidos de posición aprendibles: también se consideró el uso de vectores de embebido aprendibles para cada posición.
Code
    # Conceptual code
    positional_embeddings = nn.Embedding(max_seq_length, embedding_dim)
    positions = torch.arange(seq_length)
    positional_encoding = positional_embeddings(positions)
    final_embedding = word_embedding + positional_encoding

Este método puede aprender representaciones únicas por posición, pero aún tiene la limitación fundamental de no poder procesar secuencias más largas que los datos de entrenamiento.

Condiciones clave para la representación de información posicional

El equipo de investigación descubrió a través de ensayos y errores que la representación de información posicional debe cumplir las siguientes tres condiciones clave:

  1. Sin límite de longitud de secuencia: Debe poder representar adecuadamente posiciones no vistas durante el entrenamiento (por ejemplo, la posición 1000).
  2. Representación de relaciones de distancia relativa: La distancia entre las posiciones 2 y 4 debe ser representada de la misma manera que la distancia entre las posiciones 102 y 104. Es decir, se deben preservar las distancias relativas entre posiciones.
  3. Compatibilidad con el cálculo de atención: La información posicional debe transmitir eficazmente la información de orden sin interferir en el cálculo de los pesos de atención.

8.3.2 Diseño del codificador posicional

Tras estas reflexiones, el equipo de investigación encontró una solución innovadora llamada codificación posicional (Positional Encoding) que aprovecha las propiedades periódicas de las funciones seno (sin) y coseno (cos).

Principio de la codificación posicional basada en funciones seno-coseno

Codificando cada posición con funciones seno y coseno de diferentes frecuencias, se representa naturalmente la distancia relativa entre las posiciones.

Code
from dldna.chapter_08.positional_encoding_utils import visualize_sinusoidal_features

visualize_sinusoidal_features()

3 es una ilustración que visualiza el movimiento de la posición. Muestra cómo se expresan las relaciones de posición mediante funciones senoidales. Satisface la segunda condición, “expresión de relaciones de distancia relativa”. Todas las curvas desplazadas mantienen la misma forma que la curva original mientras mantienen una separación constante. Esto significa que si la distancia entre las posiciones es la misma (por ejemplo, 2→7 y 102→107), su relación también se expresa de manera idéntica.

4 es un mapa de calor de codificación posicional (Positional Encoding Matrix). Muestra cómo cada posición (eje vertical) tiene un patrón único (eje horizontal). Las columnas del eje horizontal representan funciones senoidales y cosenoidales de diferentes períodos, con períodos más largos hacia la derecha. Cada fila (posición) genera un patrón único a partir de las combinaciones de rojo (positivo) y azul (negativo). Al usar una variedad de frecuencias, desde períodos cortos hasta largos, se crea un patrón único para cada posición. Este enfoque satisface la primera condición, “sin límite de longitud de secuencia”. Al combinar funciones senoidales y cosenoidales de diferentes períodos, se pueden generar valores únicos de manera matemática hasta posiciones infinitas.

Utilizando esta característica matemática, el equipo de investigación implementó el algoritmo de codificación posicional de la siguiente manera.

Implementación de Codificación Posicional

Code
def positional_encoding(seq_length, d_model):
    # 1. 위치별 인코딩 행렬 생성
    position = np.arange(seq_length)[:, np.newaxis]  # [0, 1, 2, ..., seq_length-1]
    
    # 2. 각 차원별 주기 계산
    div_term = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))
    # 예: d_model=512일 때
    # div_term[0] ≈ 1.0        (가장 짧은 주기)
    # div_term[256] ≈ 0.0001   (가장 긴 주기)
    
    # 3. 짝수/홀수 차원에 사인/코사인 적용
    pe = np.zeros((seq_length, d_model))
    pe[:, 0::2] = np.sin(position * div_term)  # 짝수 차원
    pe[:, 1::2] = np.cos(position * div_term)  # 홀수 차원
    
    return pe
  • position: [0, 1, 2, ..., seq_length-1] forma de array. Representa el índice de posición de cada palabra.
  • div_term: valor que determina el período para cada dimensión. A medida que d_model aumenta, el período se alarga.
  • pe[:, 0::2] = np.sin(position * div_term): se aplica la función seno a las dimensiones con índice par.
  • pe[:, 1::2] = np.cos(position * div_term): se aplica la función coseno a las dimensiones con índice impar.

Expresión matemática

Cada dimensión de la codificación posicional se calcula según la siguiente fórmula.

  • \(PE_{(pos, 2i)} = \sin(pos / 10000^{2i/d_{\text{model}}})\)
  • \(PE_{(pos, 2i+1)} = \cos(pos / 10000^{2i/d_{\text{model}}})\)

donde

  • \(pos\): posición de la palabra (0, 1, 2, …)
  • \(i\): índice de dimensión (0, 1, 2, …, \(d_{model}\)-1)
  • \(d_{model}\): dimensión del embedding (y de la codificación posicional)

Verificación del cambio de período

Code
from dldna.chapter_08.positional_encoding_utils import show_positional_periods
show_positional_periods()
1. Periods of positional encoding:
First dimension (i=0): 1.00
Middle dimension (i=128): 100.00
Last dimension (i=255): 9646.62

2. Positional encoding formula values (10000^(2i/d_model)):
i=  0: 1.0000000000
i=128: 100.0000000000
i=255: 9646.6161991120

3. Actual div_term values (first/middle/last):
First (i=0): 1.0000000000
Middle (i=128): 0.0100000000
Last (i=255): 0.0001036633

Aquí, lo importante es el paso 3.

Code
    # 3. 짝수/홀수 차원에 사인/코사인 적용
    pe = np.zeros((seq_length, d_model))
    pe[:, 0::2] = np.sin(position * div_term)  # 짝수 차원
    pe[:, 1::2] = np.cos(position * div_term)  # 홀수 차원

El resultado muestra la variación del período según las dimensiones.

Embedding final

La codificación posicional generada pe tiene forma (seq_length, d_model), y se suma a la matriz de embeddings de palabras originales (sentence_embedding) para crear el embedding final.

Code
final_embedding = sentence_embedding + positional_encoding

Así, el embedding final agregado contiene tanto la información semántica como posicional de la palabra. Por ejemplo, la palabra “bank” puede tener diferentes valores vectoriales finales dependiendo de su posición en la oración, lo que ayuda a distinguir entre los significados de “banco” y “orilla del río”.

De esta manera, el transformer es capaz de procesar eficazmente la información secuencial sin necesidad de RNN, sentando las bases para aprovechar al máximo las ventajas del procesamiento paralelo.

Evolución del codificado posicional, técnicas más recientes y fundamentos matemáticos

En la sección 8.3.2 revisamos el codificado posicional basado en funciones seno-coseno que es fundamental para los modelos Transformer. Sin embargo, desde la publicación del artículo “Attention is All You Need”, el codificado posicional ha evolucionado en múltiples direcciones. En esta sección de profundización abordaremos exhaustivamente el codificado posicional aprendible, el codificado posicional relativo y las tendencias más recientes en investigación, analizando detalladamente la representación matemática y los pros y contras de cada técnica.

1. Codificado posicional aprendible (Learnable Positional Encoding)

  • Concepto: En lugar de funciones fijas, el modelo aprende directamente incrustaciones que expresan información de posición.

  • 1.1 Representación matemática: El codificado posicional aprendible se representa por la siguiente matriz.

    \(P \in \mathbb{R}^{L_{max} \times d}\)

    Donde \(L_{max}\) es la longitud máxima de secuencia y \(d\) es la dimensión de incrustación. La incrustación para la posición \(i\) se da por la \(i\)-ésima fila de la matriz \(P\), es decir, \(P[i,:]\).

  • 1.2 Técnicas para resolver el problema de extrapolación: Al tratar secuencias más largas que los datos de entrenamiento, surge un problema debido a la falta de información para posiciones fuera del rango aprendido. Se han investigado técnicas para abordar este problema.

    • Interpolación de posición (Chen et al., 2023): Se genera una nueva incrustación interpolando linealmente entre las incrustaciones aprendidas.

      \(P_{ext}(i) = P[\lfloor \alpha i \rfloor] + (\alpha i - \lfloor \alpha i \rfloor)(P[\lfloor \alpha i \rfloor +1] - P[\lfloor \alpha i \rfloor])\)

      Donde \(\alpha = \frac{\text{longitud de secuencia de entrenamiento}}{\text{longitud de secuencia de inferencia}}\).

    • Escalado NTK-aware (2023): Basado en la teoría del Kernel Tangente Neural (NTK), este método introduce un efecto suavizante aumentando gradualmente las frecuencias.

  • 1.3 Aplicaciones más recientes:

    • BERT: Inicialmente limitado a 512 tokens, se expandió a 1024 tokens en RoBERTa.
    • GPT-3: Tiene un límite de 2048 tokens y utiliza una técnica para aumentar gradualmente la longitud de secuencia durante el entrenamiento.
  • Ventajas:

    • Flexibilidad: Puede aprender información de posición especializada en los datos.
    • Potencial mejora en rendimiento: En ciertas tareas, puede mostrar un mejor rendimiento que las funciones fijas.
  • Desventajas:

    • Riesgo de sobreajuste: El rendimiento general puede disminuir para secuencias de longitudes no presentes en los datos de entrenamiento.
    • Dificultad en el procesamiento de secuencias largas: Se requieren técnicas adicionales para resolver el problema de extrapolación.

2. Codificado posicional relativo (Relative Positional Encoding)

  • Idea clave: En lugar de centrarse en la posición absoluta, se enfoca en la distancia relativa entre las palabras.

  • Fondo: El significado de una palabra en el lenguaje natural a menudo se ve más influenciado por su relación con las palabras cercanas que por su posición absoluta. Además, el codificado posicional absoluto tiene la desventaja de no capturar eficazmente las relaciones entre palabras distantes.

  • 2.1 Extensión matemática:

    • Fórmula de Shaw et al. (2018): En el mecanismo de atención, se añade una incrustación aprendible (\(a_{i-j}\)) que representa la distancia relativa entre los vectores Query y Key. \(e_{ij} = \frac{x_iW^Q(x_jW^K + a_{i-j})^T}{\sqrt{d}}\)

aquí \(a_{i-j} \in \mathbb{R}^d\) es un vector aprendible para la posición relativa \(i-j\).

  • Rotary Positional Encoding (RoPE): utiliza matrices de rotación para codificar posiciones relativas.

    \(\text{RoPE}(x, m) = x \odot e^{im\theta}\)

    aquí \(\theta\) es un hiperparámetro que controla la frecuencia, y \(\odot\) denota multiplicación compleja (o la matriz de rotación correspondiente).

  • Versión simplificada de T5: utiliza un sesgo aprendible \(b\) para posiciones relativas, y recorta (clipping) los valores si la distancia relativa excede un rango determinado.

    \(e_{ij} = \frac{x_iW^Q(x_jW^K)^T + b_{\text{clip}(i-j)}}{\sqrt{d}}\)

    \(b \in \mathbb{R}^{2k+1}\) es un vector de sesgo para posiciones relativas recortadas [-k, k].

  • Ventajas:

    • Mejora en la capacidad de generalización: se generaliza mejor a secuencias de longitudes no vistas durante el entrenamiento.
    • Mejora en la capacidad de capturar dependencias a largo plazo: modela más eficazmente las relaciones entre palabras distantes.
  • Desventajas:

    • Aumento de la complejidad computacional: al considerar la distancia relativa, los cálculos de atención pueden volverse más complejos. (especialmente cuando se consideran las distancias relativas para todas las pares de palabras)

3. Optimización del codificado posicional basado en CNN

  • 3.1 Aplicación de convolución por profundidad: realiza convoluciones independientes en cada canal para reducir el número de parámetros y mejorar la eficiencia computacional. \(P(i) = \sum_{k=-K}^K w_k \cdot x_{i+k}\)

    aquí \(K\) es el tamaño del kernel, y \(w_k\) son pesos aprendibles.

  • 3.2 Convoluciones multi-escala: similar a ResNet, utiliza canales de convolución paralelos para capturar información posicional en diferentes rangos.

    \(P(i) = \text{Concat}(\text{Conv}_{3x1}(x), \text{Conv}_{5x1}(x))\)

4. Dinámica del codificado posicional recursivo

  • 4.1 Codificación basada en LSTM: utiliza una red LSTM para codificar información de posición secuencial.

    \(h_t = \text{LSTM}(x_t, h_{t-1})\) \(P(t) = W_ph_t\)

  • 4.2 Variante más reciente: Neural ODE: modela dinámicas en tiempo continuo para superar las limitaciones de la LSTM discreta.

    \(\frac{dh(t)}{dt} = f_\theta(h(t), t)\) \(P(t) = \int_0^t f_\theta(h(\tau), \tau)d\tau\)

5. Interpretación cuántica del codificado posicional complejo

  • 5.1 Representación de embeddings complejos: representa la información de posición en forma compleja.

    \(z(i) = r(i)e^{i\phi(i)}\)

    aquí \(r\) es la magnitud de la posición, y \(\phi\) es el ángulo de fase.

  • 5.2 Teorema de desplazamiento de fase: representa el desplazamiento de posición como una rotación en el plano complejo.

    \(z(i+j) = z(i) \cdot e^{i\omega j}\)

    aquí \(\omega\) es un parámetro de frecuencia aprendible.

6. Enfoque híbrido

  • 6.1 Codificado posicional compuesto: \(P(i)=αP_{abs}(i)+βP_{rel}(i)\)

    \(P(i)=αP_{abs} (i)+βP_{rel}(i)\) α, β = pesos de aprendizaje

  • 6.2 Codificación Posicional Dinámica:

    \(P(i) = \text{MLP}(i, \text{Context})\) Aprendizaje de representaciones posicionales dependientes del contexto

7. Comparación de rendimiento experimental (GLUE Benchmark)

A continuación se presentan los resultados de la comparación de rendimiento experimental de diferentes enfoques de codificación posicional en el benchmark GLUE. (El rendimiento real puede variar según la estructura del modelo, los datos y la configuración de los hiperparámetros).

Método Precisión Tiempo de inferencia (ms) Uso de memoria (GB)
Absoluto (Senoidal) 88.2 12.3 2.1
Relativo (RoPE) 89.7 14.5 2.4
CNN Multiescala 87.9 13.8 3.2
Complejo (CLEX) 90.1 15.2 2.8
PE Dinámico 90.3 17.1 3.5

8. Tendencias de investigación recientes (2024)

Recientemente, se han estado investigando nuevas técnicas de codificación posicional inspiradas en sistemas cuánticos y biológicos.

  • Codificación Posicional Cuántica:
    • Uso de puertas de rotación de qubits: \(R_z(\theta_i)|x\rangle\)
    • Búsqueda de posición basada en el algoritmo de Grover
  • Codificación Bioinspirada:
    • Aplicación de la regla STDP (Spike-Timing-Dependent Plasticity) de plasticidad sináptica: \(\Delta w_{ij} \propto e^{-\frac{|i-j|}{\tau}}\)
  • Integración con Redes Neuronales de Grafos:
    • Representación de posiciones como nodos y relaciones como aristas: \(P(i) = \sum_{j \in \mathcal{N}(i)} \alpha_{ij}Wx_j\)

9. Guía de selección

  • Secuencias de longitud fija: PE aprendible. Bajo riesgo de sobreajuste y fácil optimización.
  • Longitud variable/extrapolación necesaria: RoPE. Excelente escalabilidad de longitud debido a la invarianza rotacional.
  • Procesamiento en tiempo real de baja latencia: Basado en CNN. Optimal para procesamiento paralelo y aceleración por hardware.
  • Procesamiento de señales físicas: PE complejo. Preserva información de frecuencia y compatibilidad con transformadas de Fourier.
  • Datos multimodales: PE dinámico. Adaptación receptiva al contexto cruzado modal.

Apéndice matemático

  • Características de teoría de grupos de RoPE:

    Representación del grupo de rotación SO(2): \(R(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix}\)

    Esta propiedad garantiza la preservación de la posición relativa en las puntuaciones de atención.

  • Cálculo eficiente del sesgo de posición relativa:

    Uso de la estructura de matriz Toeplitz: \(B = [b_{i-j}]_{i,j}\)

    Implementación posible con complejidad \(O(n\log n)\) utilizando FFT.

  • Flujo de gradiente de PE complejo:

    Aplicación de las reglas de diferenciación de Wirtinger: \(\frac{\partial L}{\partial z} = \frac{1}{2}\left(\frac{\partial L}{\partial \text{Re}(z)} - i\frac{\partial L}{\partial \text{Im}(z)}\right)\)


Conclusión: La codificación posicional es un elemento clave que tiene un gran impacto en el rendimiento del modelo de transformadores y ha evolucionado de diversas maneras más allá de las funciones simples de seno-coseno. Cada método tiene sus propias ventajas y desventajas, así como fundamentos matemáticos, y es importante seleccionar el método adecuado según las características y requisitos del problema. Recientemente, se han estado investigando nuevas técnicas de codificación posicional inspiradas en diversos campos, como la computación cuántica y la biología, lo que sugiere un desarrollo continuo esperado en el futuro.

8.4 Arquitectura completa del transformer

Hasta ahora hemos examinado cómo se han desarrollado los componentes clave del transformer. Ahora veamos cómo estos elementos se integran en una arquitectura completa. Esta es la arquitectura completa del transformer.

Arquitectura del transformer

Fuente de la figura: The Illustrated Transformer (Jay Alammar, 2018) Licencia CC BY 4.0

El código de implementación del transformer para fines educativos se encuentra en chapter_08/transformer. Esta implementación se basa y modifica The Annotated Transformer del grupo Harvard NLP. Los principales cambios son los siguientes.

  1. Modularización: La implementación que estaba en un solo archivo se ha dividido en varios módulos para mejorar la legibilidad y reutilizabilidad.
  2. Adopción de la estructura Pre-LN: A diferencia del artículo original, utilizamos una estructura Pre-LN donde la normalización de capa se aplica antes de las operaciones de atención/feeds-forward (recientes estudios han reportado que Pre-LN es más favorable para la estabilidad y el rendimiento del entrenamiento).
  3. Añadida clase TransformerConfig: Se ha introducido una clase separada para configurar el modelo, facilitando la gestión de hiperparámetros.
  4. Implementación estilo PyTorch: Se han utilizado funciones de PyTorch como nn.ModuleList para hacer que el código sea más conciso e intuitivo.
  5. El optimizador Noam se implementó pero no se utilizó.

8.4.1 Integración de componentes básicos

El transformer está compuesto principalmente por un codificador (Encoder) y un decodificador (Decoder), con los siguientes componentes:

Componente Codificador Decodificador
Atención multi-cabeza Atención a sí misma (Self-Attention) Atención a sí misma enmascarada (Masked Self-Attention)
Atención codificador-decodificador (Encoder-Decoder Attention)
Red de alimentación hacia adelante Aplicado independientemente en cada posición Aplicado independientemente en cada posición
Conexión residual Suma la entrada y salida de cada subcapa (atención, feed-forward) Suma la entrada y salida de cada subcapa (atención, feed-forward)
Normalización de capa Aplicada a la entrada de cada subcapa (Pre-LN) Aplicada a la entrada de cada subcapa (Pre-LN)

Capa del codificador - código

Code
class TransformerEncoderLayer(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.attention = MultiHeadAttention(config)
        self.feed_forward = FeedForward(config)
        # SublayerConnection for Pre-LN structure
        self.sublayer = nn.ModuleList([
            SublayerConnection(config) for _ in range(2)
        ])

    def forward(self, x, attention_mask=None):
        x = self.sublayer[0](x, lambda x: self.attention(x, x, x, attention_mask))
        x = self.sublayer[1](x, self.feed_forward)
        return x
  • Atención Multi-Cabeza (Multi-Head Attention): calcula las relaciones entre todas las posiciones de la secuencia de entrada en paralelo. Cada cabeza analiza la secuencia desde una perspectiva diferente y sintetiza los resultados para capturar información contextual rica. (“The cat sits on the mat” ejemplo donde diferentes cabezas aprenden relaciones sujeto-verbo, frase preposicional, artículo-sustantivo, etc.)

    • Red Feed-Forward: es una red compuesta por dos transformaciones lineales y la función de activación GELU que se aplica independientemente a cada posición.
Code
class FeedForward(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.linear1 = nn.Linear(config.hidden_size, config.intermediate_size)
        self.linear2 = nn.Linear(config.intermediate_size, config.hidden_size)
        self.activation = nn.GELU()
        
    def forward(self, x):
        x = self.linear1(x)
        x = self.activation(x)
        x = self.linear2(x)
        return x

La razón por la que se necesita una red feedforward está relacionada con la densidad de información de las salidas de atención. El resultado de la operación de atención (\(\text{Attention}(Q, K, V) = \text{softmax}(\frac{QK^T}{\sqrt{d\_k}})V\)) es una suma ponderada de los vectores \(V\), donde la información contextual está concentrada en \(d\_{model}\) dimensiones (512 en el artículo). Si se aplica directamente la función de activación ReLU, gran parte de esta información concentrada puede perderse (ReLU convierte los valores negativos en 0). Por lo tanto, la red feedforward primero expande las \(d\_{model}\) dimensiones a una dimensión mayor (\(4 \times d\_{model}\), 2048 en el artículo) para ampliar el espacio de representación, luego aplica ReLU (o GELU) y vuelve a reducir la dimensionalidad a la original, añadiendo no linealidad.

Code
x = W1(x)    # hidden_size -> intermediate_size (512 -> 2048)
x = ReLU(x)  # or GELU
x = W2(x)    # intermediate_size -> hidden_size (2048 -> 512)
  • Conexión residual (Residual Connection): Se trata de sumar la entrada y la salida de cada subcapa (atención multi-cabeza o red feedforward). Esto ayuda a mitigar el problema de desvanecimiento/explotación del gradiente y facilita el aprendizaje en redes profundas. (Consulte el Capítulo 7 sobre conexiones residuales).

  • Normalización de capa (Layer Normalization): Se aplica a la entrada de cada subcapa (Pre-LN).

Code
class LayerNorm(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.gamma = nn.Parameter(torch.ones(config.hidden_size))
        self.beta = nn.Parameter(torch.zeros(config.hidden_size))
        self.eps = config.layer_norm_eps

    def forward(self, x):
        mean = x.mean(-1, keepdim=True)
        std = (x - mean).pow(2).mean(-1, keepdim=True).sqrt()
        return self.gamma * (x - mean) / (std + self.eps) + self.beta

La normalización de capas es una técnica propuesta en el paper “Layer Normalization” de Ba, Kiros y Hinton en 2016. Mientras que la normalización por lotes (Batch Normalization) realiza la normalización a lo largo de la dimensión del lote, la normalización de capas calcula la media y la varianza a lo largo de la dimensión de características de cada muestra para realizar la normalización.

Ventajas de la normalización de capas

  1. Independencia del tamaño del lote: No se ve afectada por el tamaño del lote, funcionando de manera estable incluso con lotes pequeños o en entornos de aprendizaje en línea (online learning).
  2. Irrelevancia de la longitud de secuencia: Es adecuada para modelos que manejan secuencias de longitud variable, como RNN y Transformer.
  3. Estabilización y aceleración del aprendizaje: Estabiliza la distribución de entrada de cada capa, mitigando problemas de desvanecimiento o explosión de gradientes y aumentando la velocidad de aprendizaje.

En el caso del Transformer, se utiliza el método Pre-LN para aplicar la normalización de capas antes de pasar por cada subcapa (atención multi-cabeza, red feed-forward).

Visualización de la normalización de capas

Code
from dldna.chapter_08.visualize_layer_norm import visualize_layer_normalization
visualize_layer_normalization()

========================================
Input Data Shape: (2, 5, 6)
Mean Shape: (2, 5, 1)
Standard Deviation Shape: (2, 5, 1)
Normalized Data Shape: (2, 5, 6)
Gamma (Scale) Values:
 [0.95208258 0.9814341  0.8893665  0.88037934 1.08125258 1.135624  ]
Beta (Shift) Values:
 [-0.00720101  0.10035329  0.0361636  -0.06451198  0.03613956  0.15380366]
Scaled & Shifted Data Shape: (2, 5, 6)
========================================

La imagen anterior muestra cómo funciona la normalización de capa (Layer Normalization) paso a paso.

  • Datos originales (superior izquierda): Los datos antes de la normalización están ampliamente dispersos, con una media y desviación estándar variables.
  • Después de la normalización (superior derecha): Los datos se agrupan alrededor de una media de 0 y una desviación estándar cercana a 1.
  • Escala y desplazamiento (centro): Se aplican parámetros aprendibles γ (gamma, escala) y β (beta, desplazamiento) para introducir pequeñas variaciones en la distribución de los datos. Esto ajusta la capacidad expresiva del modelo.
  • Mapa de calor (inferior): Muestra el cambio individual de valores antes y después de la normalización, así como después de aplicar la escala/desplazamiento, basándose en los datos del primer lote.
  • Valores γ/β (inferior derecha): Representa los valores de γ y β para cada dimensión oculta mediante gráficos de barras.

De esta manera, la normalización de capa normaliza las entradas de cada capa para mejorar la estabilidad y velocidad del aprendizaje.

Clave:

  • Normalización de entrada de cada capa (media 0, desviación estándar 1)
  • Ajuste de capacidad expresiva con escalas (γ) y desplazamientos (β) aprendibles
  • Mantenimiento de independencia entre muestras, a diferencia de la normalización por lotes

Esta combinación de componentes (atención multi-cabeza, red feedforward, conexión residual, normalización de capa) maximiza las ventajas de cada elemento. La atención multi-cabeza captura diversos aspectos de la secuencia de entrada, la red feedforward añade no linealidad, y las conexiones residuales y la normalización de capa permiten un aprendizaje estable incluso en redes profundas.

8.4.2 Composición del codificador

El transformador tiene una estructura de codificador-decodificador para la traducción automática. El codificador comprende el idioma de origen (por ejemplo, inglés) y el decodificador genera el idioma objetivo (por ejemplo, francés). Aunque el codificador y el decodificador comparten componentes básicos como la atención multi-cabeza y las redes feedforward, están configurados de manera diferente para adaptarse a sus respectivos propósitos.

Comparación de la composición del codificador vs. decodificador

Componente Codificador Decodificador
Número de capas de atención 1 (autoatención) 2 (autoatención enmascarada, atención codificador-decodificador)
Estrategia de máscara Solo utiliza máscaras de relleno Máscaras de relleno + máscaras de causalidad
Procesamiento contextual Procesamiento de contexto bidireccional Procesamiento de contexto unidireccional (autoregresivo)
Referencia de entrada Solo se refiere a su propia entrada Se refiere a su propia entrada + salida del codificador

A continuación, se resumen varios términos de atención.

Resumen de conceptos de atención

Tipo de atención Características Ubicación de descripción Concepto clave
Atención (básica) - Cálculo de similitud mediante vectores de palabras idénticos
- Generación de información contextual a través de una simple suma ponderada
- Versión simplificada para aplicar en modelos seq2seq
8.2.2 - Cálculo de similitud mediante producto interno de vectores de palabras
- Conversión de pesos con softmax
- Aplicación de máscaras de relleno a toda la atención
Autoatención (Self-Attention) - Separación del espacio Q, K, V
- Optimización independiente en cada espacio
- La secuencia de entrada se refiere a sí misma
- Utilizado en el codificador
8.2.3 - Separación de roles para cálculo de similitud y transmisión de información
- Transformaciones aprendibles Q, K, V
- Posibilidad de procesamiento contextual bidireccional
Autoatención enmascarada - Bloqueo de información futura
- Uso de máscaras causales
- Utilizado en el decodificador
8.2.5 - Máscara triangular superior para bloquear la información futura
- Posibilidad de generación autoregresiva
- Procesamiento contextual unidireccional
Atención cruzada (codificador-decodificador) - Query: estado del decodificador
- Key, Value: salida del codificador
- También llamada atención cruzada
- Utilizado en el decodificador
8.4.3 - El decodificador hace referencia a la información del codificador
- Cálculo de relaciones entre dos secuencias
- Refleja el contexto durante la traducción/generación

En el transformador, se utilizan los nombres autoatención, enmascarada y cruzada. Mecanismos de atención son idénticos y se distinguen según la fuente de Q, K, V.

Componentes del codificador | Componente | Descripción | | —————————- | ——————————————————————————————— | | Embeddings | Convierte los tokens de entrada en vectores y agrega información de posición para codificar el significado y el orden de la secuencia de entrada. | | TransformerEncoderLayer (x N) | Apila múltiples capas de la misma capa para extraer características más abstractas y complejas de forma jerárquica a partir de la secuencia de entrada. | | LayerNorm | Normaliza la distribución de las características de la salida final para estabilizarla y prepararla en un formato útil para el decodificador. |

Code
class TransformerEncoder(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.embeddings = Embeddings(config)
        self.layers = nn.ModuleList([
            TransformerEncoderLayer(config) 
            for _ in range(config.num_hidden_layers)
        ])
        self.norm = LayerNorm(config)
    
    def forward(self, input_ids, attention_mask=None):
        x = self.embeddings(input_ids)

        for i, layer in enumerate(self.layers):
            x = layer(x, attention_mask)
        
        output = self.norm(x)
        return output

El codificador está compuesto por una capa de incrustación, múltiples capas de codificador y una capa de normalización final.

1. Mecanismo de autoatención (ejemplo)

La autoatención del codificador calcula las relaciones entre todas las pares de palabras en la secuencia de entrada para enriquecer la información contextual de cada palabra.

  • Ejemplo: “The patient bear can bear the pain no longer.”
  • Función: Al determinar el significado del segundo ‘bear’, la autoatención considera la relación con todas las palabras en la oración, como ‘patient’ (paciente), ‘bear’ (oso) y ‘pain’ (dolor). De esta manera, se puede determinar con precisión que ‘bear’ se usa en el sentido de ‘soportar’, ‘aguantar’ (procesamiento contextual bidireccional).

2. Importancia de la posición del dropout

El dropout desempeña un papel crucial para prevenir el sobreajuste y mejorar la estabilidad del aprendizaje. En el codificador de transformers, se aplica dropout en las siguientes posiciones.

  • Después de la salida de incrustación: Inmediatamente después de que los embeddings de tokens y la información de posición se combinan.
  • Después de cada subcapa (atención, FFN): Sigue una estructura Pre-LN (normalización → subcapa → dropout → conexión residual).
  • En el interior del FFN: Después de aplicar la primera transformación lineal y la función de activación ReLU.

Este esquema de aplicación de dropout regula el flujo de información, evita que el modelo dependa excesivamente de ciertas características y mejora el rendimiento generalización.

3. Estructura de pila del codificador

El codificador de transformers tiene una estructura en la que se apilan (stack) varias capas de codificador con la misma arquitectura.

  • Papel original: Se utilizan 6 capas de codificador.
  • División de funciones:
    • Capas inferiores: Aprendizaje de patrones lingüísticos superficiales, como palabras adyacentes y puntuación.
    • Capas intermedias: Aprendizaje de estructuras gramaticales.
    • Capas superiores: Aprendizaje de relaciones semánticas de nivel superior, como la referencia cruzada (coreference).

Cuanto más profundas son las capas, más características abstractas y complejas pueden aprender. Investigaciones posteriores han permitido el desarrollo de modelos con muchas más capas gracias a los avances en hardware y técnicas de aprendizaje (Pre-LayerNorm, recorte de gradientes, calentamiento del tasa de aprendizaje, aprendizaje de precisión mixta, acumulación de gradientes), como BERT-base: 12 capas, GPT-3: 96 capas, PaLM: 118 capas.

4. Salida final del codificador y uso en el decodificador

La salida final del codificador es una representación vectorial que contiene información contextual rica para cada token de entrada. Esta salida se utiliza como Key y Value en la atención codificador-decodificador (Cross-Attention) del decodificador. El decodificador genera cada token de la secuencia de salida consultando la salida del codificador, lo que permite una traducción/creación precisa considerando el contexto de la oración original.

8.4.3 Estructura del decodificador

El decodificador es similar al codificador, pero difiere en que genera la salida de manera autoregresiva (autoregressive).

Código completo de la capa del decodificador

Code
class TransformerDecoderLayer(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.self_attn = MultiHeadAttention(config)
        self.cross_attn = MultiHeadAttention(config)
        self.feed_forward = FeedForward(config)
        
        # Pre-LN을 위한 레이어 정규화
        self.norm1 = LayerNorm(config)
        self.norm2 = LayerNorm(config)
        self.norm3 = LayerNorm(config)
        self.dropout = nn.Dropout(config.dropout_prob)

    def forward(self, x, memory, src_mask=None, tgt_mask=None):
        # Pre-LN 구조
        m = self.norm1(x)
        x = x + self.dropout(self.self_attn(m, m, m, tgt_mask))
        
        m = self.norm2(x)
        x = x + self.dropout(self.cross_attn(m, memory, memory, src_mask))
        
        m = self.norm3(x)
        x = x + self.dropout(self.feed_forward(m))
        return x

Componentes principales del decodificador y sus roles

Subcapa Rol Características de implementación
Atención propia enmascarada Identificar las relaciones entre las palabras dentro de la secuencia de salida generada hasta el momento, evitar la referencia a información futura (generación autoregresiva) tgt_mask (máscara causal + máscara de padding) usada, self.self_attn
Atención codificador-decodificador (atención cruzada) El decodificador se refiere a la salida del codificador (información contextual de la oración de entrada) para obtener información relevante al palabra actual que se está generando Q: decodificador, K, V: codificador, src_mask (máscara de padding) usada, self.cross_attn
Red de alimentación hacia adelante Transformar independientemente las representaciones en cada posición para generar representaciones más ricas Estructura idéntica a la del codificador, self.feed_forward
Normalización de capa (LayerNorm) Normalizar la entrada de cada subcapa (Pre-LN), mejorar la estabilidad y rendimiento del aprendizaje self.norm1, self.norm2, self.norm3
Apagado aleatorio (Dropout) Prevenir el sobreajuste, mejorar el rendimiento generalización Aplicada a la salida de cada subcapa, self.dropout
Conexión residual (Residual Connection) Mitigar problemas de desvanecimiento/explotación del gradiente en redes profundas, mejorar el flujo de información Sumar la entrada y la salida de cada subcapa

1. Atención propia enmascarada (Masked Self-Attention) * Rol: El decodificador genera la salida de manera autoregresiva (autoregressive), es decir, no puede hacer referencia a las palabras que futuras aparecerán después de la palabra actualmente en generación. Por ejemplo, al traducir “I love you”, una vez generado “yo” y al generar “te”, no se puede acceder al token “amo” que aún no ha sido generado. * Implementación: Se utiliza tgt_mask, que combina una máscara de causalidad (causal mask) y una máscara de padding. La máscara de causalidad llena la parte superior triangular de la matriz con -inf para hacer que los pesos de atención a tokens futuros sean 0 (ver sección 8.2.5). En el método forward de TransformerDecoderLayer, esta máscara se aplica en la parte self.self_attn(m, m, m, tgt_mask).

2. Atención codificador-decodificador (Cross-Attention)

  • Rol: El decodificador se refiere a la salida del codificador (la información contextual de la oración de entrada) para obtener información relevante al generar la palabra actual. Esto es crucial en tareas de traducción, ya que permite que el decodificador comprenda con precisión el significado de la oración original y seleccione las palabras de traducción adecuadas.
  • Implementación:
    • Query (Q): El estado actual del decodificador (la salida de la autoatención enmascarada)
    • Key (K): La salida del codificador (memory)
    • Value (V): La salida del codificador (memory)
    • Se utiliza src_mask (máscara de padding) para ignorar los tokens de padding en la salida del codificador.
    • En el método forward de TransformerDecoderLayer, esta atención se realiza en la parte self.cross_attn(m, memory, memory, src_mask). memory representa la salida del codificador.

3. Estructura de pila del decodificador

Code
class TransformerDecoder(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.embeddings = Embeddings(config)
        self.layers = nn.ModuleList([
            TransformerDecoderLayer(config)
            for _ in range(config.num_hidden_layers)
        ])
        self.norm = LayerNorm(config)

    def forward(self, x, memory, src_mask=None, tgt_mask=None):
        x = self.embeddings(x)
        for layer in self.layers:
            x = layer(x, memory, src_mask, tgt_mask)
        return self.norm(x)
  • El decodificador está compuesto por varias capas de TransformerDecoderLayer (6 capas en el artículo original).
  • Cada capa ejecuta secuencialmente la autoatención mascara, la atención codificador-decodificador y la red feedforward.
  • La estructura Pre-LN y las conexiones residuales se aplican a cada subcapa. Esto permite un aprendizaje estable incluso en redes profundas.
  • El método forward de la clase TransformerDecoder recibe como entrada x (entrada del decodificador), memory (salida del codificador), src_mask (máscara de padding del codificador) y tgt_mask (máscara del decodificador), pasa secuencialmente a través de las capas del decodificador y devuelve la salida final.

Número de capas de codificador/decodificador por modelo

Modelo Año Estructura Capas de codificador Capas de decodificador Parámetros totales
Transformer original 2017 Codificador-decodificador 6 6 65M
BERT-base 2018 Solo codificador 12 - 110M
GPT-2 2019 Solo decodificador - 48 1.5B
T5-base 2020 Codificador-decodificador 12 12 220M
GPT-3 2020 Solo decodificador - 96 175B
PaLM 2022 Solo decodificador - 118 540B
Gemma-2 2024 Solo decodificador - 18-36 2B-27B

Los modelos recientes pueden aprender eficazmente un mayor número de capas gracias a técnicas avanzadas de aprendizaje como Pre-LN. Los decodificadores más profundos pueden aprender patrones lingüísticos más abstractos y complejos, lo que ha llevado a mejoras en el rendimiento en tareas de procesamiento de lenguaje natural como la traducción y la generación de texto.

4. Generación de salida del decodificador y condiciones de finalización

  • Generación de salida: La capa generator (capa lineal) de la clase Transformer convierte la salida final del decodificador en un vector de logit de tamaño de vocabulario (vocab_size) y aplica log_softmax para obtener la distribución de probabilidad de cada token. Se predice el siguiente token basado en esta distribución de probabilidad.
Code
# 최종 출력 생성 (설명용)
output = self.generator(decoder_output)
return F.log_softmax(output, dim=-1)
  • Condiciones de terminación
    1. Alcanzar la longitud máxima: cuando se alcanza la longitud de salida predeterminada.
    2. Condiciones de terminación definidas por el usuario: cuando se cumple una condición específica (por ejemplo, un signo de puntuación).
    3. Generación de tokens especiales: cuando se genera un token especial que indica el final de la frase (<eos>, </s> etc.). El decodificador aprende durante el proceso de entrenamiento a agregar este token especial al final de las frases.
  • Estrategias de generación de tokens

En general, aunque no están incluidas en el decodificador, las estrategias de generación de tokens pueden influir en los resultados de la salida generada.

Estrategia de generación Cómo funciona Ventajas Desventajas Ejemplo
Greedy Search En cada paso, selecciona el token con la probabilidad más alta. Rápido, implementación simple Posibilidad de solución subóptima local, falta de diversidad “Yo” siguiente → “voy a la escuela” (máxima probabilidad)
Beam Search Seguimiento simultáneo de k caminos. Búsqueda amplia, posibilidad de mejores resultados Costo computacional alto, diversidad limitada k=2: mantener “Yo voy a la escuela”, “Yo voy a casa” y proceder al siguiente paso
Top-k Sampling Selecciona proporcionalmente según la probabilidad entre los k tokens más probables. Diversidad adecuada, evita tokens extraños Dificultad en establecer el valor de k, rendimiento dependiente del contexto k=3: “Yo” siguiente → {“voy a la escuela”, “voy a casa”, “voy al parque”} seleccionado según la probabilidad
Nucleus Sampling Selecciona entre los tokens cuya probabilidad acumulada no excede p. Grupo de candidatos dinámico, flexible con el contexto Necesidad de ajustar el valor de p, aumento de complejidad computacional p=0.9: “Yo” siguiente → {“voy a la escuela”, “voy a casa”, “voy al parque”, “como”} seleccionado sin exceder una probabilidad acumulada de 0.9
Temperature Sampling Ajuste de la distribución de probabilidad (baja para ser más seguro, alta para ser más diverso). Regulación de la creatividad de la salida, implementación simple Demasiado alto puede resultar inapropiado, demasiado bajo puede generar texto repetitivo T=0.5: enfatiza las altas probabilidades, T=1.5: aumenta la posibilidad de seleccionar bajas probabilidades

Estas estrategias de generación de tokens se implementan generalmente como clases o funciones separadas del decodificador.

8.4.4 Explicación de la estructura completa

Hasta ahora hemos entendido el propósito del diseño y el principio de funcionamiento del transformer. Con base en lo explicado hasta el 8.4.3, examinaremos la estructura completa del transformer. La implementación se ha modificado estructuralmente, incluyendo modularización, basándose en el contenido de Havard NLP, y se ha redactado de manera lo más concisa posible para fines educativos. En un entorno de producción real, se necesitarían adiciones como tipado de tipos para la estabilidad del código, procesamiento eficiente de tensores multidimensionales, validación de entrada y manejo de errores, optimización de memoria, y escalabilidad para soportar diversas configuraciones.

El código está en el directorio chapter_08/transformer.

Función e implementación de la capa de incrustación

La primera etapa del transformer es la capa de incrustación, que convierte los tokens de entrada en un espacio vectorial. La entrada es una secuencia de IDs de tokens enteros (por ejemplo: [101, 2045, 3012, …]), donde cada ID de token es un índice único del diccionario léxico. La capa de incrustación mapea estos IDs a vectores de alta dimensión (vectores de incrustación).

La dimensión de incrustación tiene un gran impacto en el rendimiento del modelo. Una dimensión grande puede representar información semántica rica, pero aumenta el costo computacional, mientras que una dimensión pequeña tiene el efecto contrario.

Tras pasar por la capa de incrustación, las dimensiones del tensor cambian de la siguiente manera:

  • Entrada: (batch_size, seq_length) → Salida: (batch_size, seq_length, hidden_size)
  • Ejemplo: (32, 50) → (32, 50, 768)

A continuación se muestra un ejemplo de código para realizar incrustaciones en el transformer.

Code
import torch
from dldna.chapter_08.transformer.config import TransformerConfig
from dldna.chapter_08.transformer.embeddings import Embeddings

# Create a configuration object
config = TransformerConfig()
config.vocab_size = 1000  # Vocabulary size
config.hidden_size = 768  # Embedding dimension
config.max_position_embeddings = 512  # Maximum sequence length

# Create an embedding layer
embedding_layer = Embeddings(config)

# Generate random input tokens
batch_size = 2
seq_length = 4
input_ids = torch.tensor([
    [1, 5, 9, 2],  # First sequence
    [6, 3, 7, 4]   # Second sequence
])

# Perform embedding
embedded = embedding_layer(input_ids)

print(f"Input shape: {input_ids.shape}")
# Output: Input shape: torch.Size([2, 4])

print(f"Shape after embedding: {embedded.shape}")
# Output: Shape after embedding: torch.Size([2, 4, 768])

print("\nPart of the embedding vector for the first token of the first sequence:")
print(embedded[0, 0, :10])  # Print only the first 10 dimensions
Input shape: torch.Size([2, 4])
Shape after embedding: torch.Size([2, 4, 768])

Part of the embedding vector for the first token of the first sequence:
tensor([-0.7838, -0.9194,  0.4240, -0.8408, -0.0876,  2.0239,  1.3892, -0.4484,
        -0.6902,  1.1443], grad_fn=<SliceBackward0>)

Clase de configuración

La clase TransformerConfig define todos los hiperparámetros del modelo.

Code
class TransformerConfig:
    def __init__(self):
        self.vocab_size = 30000          # Vocabulary size
        self.hidden_size = 768           # Hidden layer dimension
        self.num_hidden_layers = 12      # Number of encoder/decoder layers
        self.num_attention_heads = 12    # Number of attention heads
        self.intermediate_size = 3072    # FFN intermediate layer dimension
        self.hidden_dropout_prob = 0.1   # Hidden layer dropout probability
        self.attention_probs_dropout_prob = 0.1  # Attention dropout probability
        self.max_position_embeddings = 512  # Maximum sequence length
        self.layer_norm_eps = 1e-12      # Layer normalization epsilon

vocab_size es el número total de tokens únicos que el modelo puede manejar. Aquí, para una implementación simple, asumimos la tokenización a nivel de palabras y lo establecemos en 30,000. En modelos de lenguaje reales, se utilizan diversos tokenizadores subpalabra como BPE (Byte Pair Encoding), Unigram, WordPiece, etc., y en este caso, vocab_size puede ser menor. Por ejemplo, la palabra ‘playing’ puede descomponerse en ‘play’ e ‘ing’, lo que permite representarla con solo dos subpalabras.

Cambio de dimensiones del tensor de atención

En la atención multi-cabeza, cada cabeza reorganiza las dimensiones del tensor de entrada para calcular la atención de manera independiente.

Code
class MultiHeadAttention(nn.Module):
    def forward(self, query, key, value, mask=None):
        batch_size = query.size(0)
        
        # Linear transformations and head splitting
        query = self.linears[0](query).view(batch_size, -1, self.h, self.d_k).transpose(1, 2)
        key = self.linears[1](key).view(batch_size, -1, self.h, self.d_k).transpose(1, 2)
        value = self.linears[2](value).view(batch_size, -1, self.h, self.d_k).transpose(1, 2)

El proceso de transformación dimensional es el siguiente:

  1. Entrada: (batch_size, seq_len, d_model)
  2. Transformación lineal: (batch_size, seq_len, d_model)
  3. view: (batch_size, seq_len, h, d_k)
  4. transpose: (batch_size, h, seq_len, d_k)

Aquí, h es el número de cabezas y d_k es la dimensión de cada cabeza (d_model / h). A través de este reordenamiento dimensional, cada cabeza calcula la atención de manera independiente.

Estructura integrada del transformador

Finalmente, examinemos la clase Transformer que integra todos los componentes.

Code
class Transformer(nn.Module):
    def __init__(self, config: TransformerConfig):
        super().__init__()
        self.encoder = TransformerEncoder(config)
        self.decoder = TransformerDecoder(config)
        self.generator = nn.Linear(config.hidden_size, config.vocab_size)
        self._init_weights()

El transformador consta de tres componentes principales.

  1. Codificador: procesa la secuencia de entrada.
  2. Decodificador: genera la secuencia de salida.
  3. Generador: convierte la salida del decodificador en probabilidades léxicas.

El método forward procesa los datos en el siguiente orden.

Code
def forward(self, src, tgt, src_mask=None, tgt_mask=None):
    # Encoder-decoder processing
    encoder_output = self.encode(src, src_mask)
    decoder_output = self.decode(encoder_output, src_mask, tgt, tgt_mask)
    
    # Generate final output
    output = self.generator(decoder_output)
    return F.log_softmax(output, dim=-1)

El cambio de dimensiones del tensor es el siguiente:

  1. Entrada (src, tgt): (batch_size, seq_len)
  2. Salida del codificador: (batch_size, src_len, hidden_size)
  3. Salida del decodificador: (batch_size, tgt_len, hidden_size)
  4. Salida final: (batch_size, tgt_len, vocab_size)

En la siguiente sección aplicaremos esta estructura a un ejemplo práctico.

8.5 Ejemplos de transformer

Hasta ahora hemos examinado la estructura y el principio de funcionamiento del transformer. Ahora, a través de ejemplos prácticos, veremos cómo funciona el transformer. Los ejemplos están organizados en orden de dificultad, y cada uno está diseñado para ayudar a comprender una función específica del transformer. Estos ejemplos muestran métodos para resolver gradualmente diversos problemas de procesamiento de datos y diseño de modelos que pueden encontrarse en proyectos reales. En particular, abordan temas prácticos como el preprocesamiento de datos, el diseño de funciones de pérdida y la configuración de métricas de evaluación. La ubicación de los ejemplos es transformer/examples.

examples
├── addition_task.py  # 8.5.2 Tarea de suma
├── copy_task.py      # 8.5.1 Tarea de copia simple
└── parser_task.py    # 8.5.3 Tarea de análisis sintáctico

Lo que se aprende en cada ejemplo es lo siguiente.

Tarea de copia simple: permite comprender las funciones básicas del transformer. A través de la visualización de patrones de atención, se puede entender claramente el principio de funcionamiento del modelo. Además, se pueden aprender métodos básicos de procesamiento de datos secuenciales, diseño de dimensiones de tensores para procesamiento por lotes, estrategias básicas de padding y masking, y diseño de funciones de pérdida especializadas en la tarea.

Problema de suma posicional: muestra cómo es posible la generación autoregresiva. Se puede observar claramente el proceso de generación secuencial del decodificador y el papel de la atención cruzada. Además, proporciona experiencia práctica en tokenización de datos numéricos, métodos para generar conjuntos de datos válidos, evaluación de precisión parcial/completa, y pruebas de rendimiento generalizado según la expansión posicional.

Tarea de análisis sintáctico: muestra cómo el transformer aprende y expresa relaciones estructurales. Se puede entender cómo el mecanismo de atención captura la estructura jerárquica de secuencias de entrada. Además, se pueden adquirir diversas técnicas necesarias para problemas de análisis sintáctico prácticos, como la transformación de datos estructurados en secuencias, diseño de vocabulario de tokens, estrategias de linealización de estructuras de árboles y métodos de evaluación de precisión estructural.

A continuación se muestra una tabla que resume lo que se aprende en cada ejemplo:

Ejemplo Contenido de aprendizaje
8.5.1 Tarea de copia simple (copy_task.py) - Comprensión de las funciones básicas y el principio de funcionamiento del transformer
- Comprensión intuitiva a través de la visualización de patrones de atención
- Métodos de procesamiento de datos secuenciales y diseño de dimensiones de tensores para procesamiento por lotes
- Estrategias básicas de padding y masking, y diseño de funciones de pérdida especializadas en la tarea
8.5.2 Tarea de suma posicional (addition_task.py) - Comprensión de cómo es posible la generación autoregresiva
- Observación del proceso de generación secuencial del decodificador y el papel de la atención cruzada
- Tokenización de datos numéricos, métodos para generar conjuntos de datos válidos
- Evaluación de precisión parcial/completa, pruebas de rendimiento generalizado según la expansión posicional
8.5.3 Tarea de análisis sintáctico (parser_task.py) - Comprensión de cómo el transformer aprende y expresa relaciones estructurales
- Comprensión del mecanismo de atención que captura la estructura jerárquica de secuencias de entrada
- Transformación de datos estructurados en secuencias, diseño de vocabulario de tokens
- Estrategias de linealización de estructuras de árboles, métodos de evaluación de precisión estructural

8.5.1 Tarea de copia simple

El primer ejemplo es una tarea de copia que reproduce la secuencia de entrada tal cual. Esta tarea es adecuada para verificar el funcionamiento básico del transformer y visualizar los patrones de atención, aunque parezca sencilla, es muy útil para comprender los mecanismos centrales del transformer.

Preparación de datos

Los datos para la tarea de copia consisten en secuencias idénticas de entrada y salida. A continuación se muestra un ejemplo de generación de datos:

8.5.1 Tarea de copia simple

El primer ejemplo es una tarea de copia que reproduce la secuencia de entrada tal cual. Esta tarea es adecuada para verificar el funcionamiento básico del transformer y visualizar los patrones de atención, aunque parezca sencilla, es muy útil para comprender los mecanismos centrales del transformer.

Preparación de datos

Los datos para la tarea de copia consisten en secuencias idénticas de entrada y salida. A continuación se muestra un ejemplo de generación de datos:

Code
from dldna.chapter_08.transformer.examples.copy_task import explain_copy_data

explain_copy_data(seq_length=5)

=== Copy Task Data Explanation ===
Sequence Length: 5

1. Input Sequence:
Original Tensor Shape: torch.Size([1, 5])
Input Sequence: [7, 15, 2, 3, 12]

2. Target Sequence:
Original Tensor Shape: torch.Size([1, 5])
Target Sequence: [7, 15, 2, 3, 12]

3. Task Description:
- Basic task of copying the input sequence as is
- Tokens at each position are integer values between 1-19
- Input and output have the same sequence length
- Current Example: [7, 15, 2, 3, 12] → [7, 15, 2, 3, 12]

create_copy_data genera un tensor de salida idéntico al tensor de entrada para el aprendizaje. Crea un tensor bidimensional (batch_size, seq_length) para el procesamiento por lotes, donde cada elemento es un valor entero entre 1 y 19.

Code
def create_copy_data(batch_size: int = 32, seq_length: int = 5) -> torch.Tensor:
    """복사 태스크용 데이터 생성"""
    sequences = torch.randint(1, 20, (batch_size, seq_length))
    return sequences, sequences

Este ejemplo de datos es idéntico a los datos de entrada tokenizados utilizados en el procesamiento de lenguaje natural y modelado de secuencias. En el procesamiento de lenguaje, cada token se convierte en un valor entero único antes de ser ingresado al modelo.

Entrenamiento del modelo

Se entrena el modelo con el siguiente código.

Code
from dldna.chapter_08.transformer.config import TransformerConfig
from dldna.chapter_08.transformer.examples.copy_task import train_copy_task

seq_length = 20
config = TransformerConfig()
# Modify default values
config.vocab_size = 20           # Small vocabulary size (minimum size to represent integers 1-19)
config.hidden_size = 64          # Small hidden dimension (enough representation for a simple task)
config.num_hidden_layers = 2     # Minimum number of layers (considering the low complexity of the copy task)
config.num_attention_heads = 2   # Minimum number of heads (minimum configuration for attention from various perspectives)
config.intermediate_size = 128   # Small FFN dimension (set to twice the hidden dimension to ensure adequate transformation capacity)
config.max_position_embeddings = seq_length  # Short sequence length (set to the same length as the input sequence)

model = train_copy_task(config, num_epochs=50, batch_size=40, steps_per_epoch=100, seq_length=seq_length)

=== Start Training ==== 
Device: cuda:0
Model saved to saved_models/transformer_copy_task.pth

Prueba del modelo

Se lee el modelo de entrenamiento almacenado y se realiza la prueba.

Code
from dldna.chapter_08.transformer.examples.copy_task import test_copy

test_copy(seq_length=20)

=== Copy Test ===
Input: [10, 10, 2, 12, 1, 5, 3, 1, 8, 18, 2, 19, 2, 2, 8, 14, 7, 19, 5, 4]
Output: [10, 10, 2, 12, 1, 5, 3, 1, 8, 18, 2, 19, 2, 2, 8, 14, 7, 19, 5, 4]
Accuracy: True

Configuración del modelo

  • hidden_size: 64 (dimensión de diseño del modelo, d_model).
    • En el transformer, la dimensión de diseño (d_model) es igual a:
      1. Dimensión de los embeddings de palabras.
      2. Dimensión de los embeddings posicionales.
      3. Dimensión de los vectores Q, K, V en la atención.
      4. Dimensión de las salidas de cada subcapa del codificador/decodificador.
  • intermediate_size: Tamaño del FFN, que debe ser suficientemente grande en comparación con d_model.

Implementación de máscaras

El transformer utiliza dos tipos de máscaras.

  1. Máscara de relleno (Padding Mask): Ignora los tokens de relleno añadidos para el procesamiento por lotes.
    • En este ejemplo, las secuencias tienen la misma longitud (seq_length) y no requieren relleno, pero se incluye para ilustrar el uso general del transformer.
    • Implementa la función create_pad_mask (en PyTorch, nn.Transformer o en la biblioteca transformers de Hugging Face, esta función está implementada internamente).
Code
src_mask = create_pad_mask(src).to(device)
  1. Máscara subsiguiente (Subsequent Mask): Se utiliza para la generación autoregresiva del decodificador.
  • La función create_subsequent_mask genera una máscara en forma de matriz triangular superior que oculta los tokens posteriores a la posición actual.
  • Esto hace que el decodificador solo pueda referirse a los tokens previamente generados para predecir el siguiente token.
Code
tgt_mask = create_subsequent_mask(decoder_input.size(1)).to(device)

Esta máscara garantiza la eficiencia del procesamiento por lotes y la causalidad de las secuencias.

Diseño de la función de pérdida

La clase CopyLoss implementa una función de pérdida para la tarea de copia.

  • Considera tanto la precisión en cada posición de token como si toda la secuencia coincide completamente.
  • Monitorea detalladamente la precisión, la pérdida y los valores predichos/reales para comprender finamente el progreso del entrenamiento.
Code
class CopyLoss(nn.Module):
    def forward(self, outputs: torch.Tensor, target: torch.Tensor, 
                print_details: bool = False) -> Tuple[torch.Tensor, float]:
        batch_size = outputs.size(0)
        predictions = F.softmax(outputs, dim=-1)
        target_one_hot = F.one_hot(target, num_classes=outputs.size(-1)).float()
        
        loss = -torch.sum(target_one_hot * torch.log(predictions + 1e-10)) / batch_size
        
        with torch.no_grad():
            pred_tokens = predictions.argmax(dim=-1)
            exact_match = (pred_tokens == target).all(dim=1).float()
            match_rate = exact_match.mean().item()
  • Entropía cruzada sola no es suficiente: precisión de token individual + evaluación de coincidencia de secuencia completa.
  • Fomentar que el modelo aprenda el orden correctamente.

Ejemplo de funcionamiento (batch_size=2, sequence_length=3, vocab_size=5):

  1. Salida del modelo (logits)
Code
# Example: batch_size=2, sequence_length=3, vocab_size=5 (example is vocab_size=20)

# 1. Model Output (logits)
outputs = [
    # First batch
    [[0.9, 0.1, 0.0, 0.0, 0.0],  # First position: token 0 has the highest probability
     [0.1, 0.8, 0.1, 0.0, 0.0],  # Second position: token 1 has the highest probability
     [0.0, 0.1, 0.9, 0.0, 0.0]], # Third position: token 2 has the highest probability
    # Second batch
    [[0.8, 0.2, 0.0, 0.0, 0.0],
     [0.1, 0.7, 0.2, 0.0, 0.0],
     [0.1, 0.1, 0.8, 0.0, 0.0]]
]
  1. Objetivo real
Code
# 2. Actual Target
target = [
    [0, 1, 2],  # Correct sequence for the first batch
    [0, 1, 2]   # Correct sequence for the second batch
]
  1. Proceso de cálculo de la pérdida
    • predictions = softmax(outputs) (ya convertido a probabilidades anteriormente)
    • Convertir target a vector one-hot
Code
# 3. Loss Calculation Process
# predictions = softmax(outputs) (already converted to probabilities above)
# Convert target to one-hot vectors:
target_one_hot = [
    [[1,0,0,0,0], [0,1,0,0,0], [0,0,1,0,0]],  # First batch
    [[1,0,0,0,0], [0,1,0,0,0], [0,0,1,0,0]]   # Second batch
]
  1. cálculo de precisión
Code
# 4. Accuracy Calculation
pred_tokens = [
    [0, 1, 2],  # First batch prediction
    [0, 1, 2]   # Second batch prediction
]
  • Coincidencia exacta de la secuencia completa: exact_match = [True, True] (ambos lotes son exactos)
  • Precisión promedio: match_rate = 1.0 (100%)
  1. Valor de pérdida final: media de la entropía cruzada
Code
# Exact sequence match
exact_match = [True, True]  # Both batches match exactly
match_rate = 1.0  # Average accuracy 100%

# The final loss value is the average of the cross-entropy
# loss = -1/2 * (log(0.9) + log(0.8) + log(0.9) + log(0.8) + log(0.7) + log(0.8))

Visualización de atención

A través de la visualización de atención, se puede entender de manera intuitiva el funcionamiento del transformador.

Code
from dldna.chapter_08.transformer.examples.copy_task import visualize_attention
visualize_attention(seq_length=20)

Verifica cómo cada token de entrada interactúa con tokens en otras posiciones.

A través de este ejemplo de tarea de copia, hemos examinado el mecanismo central del transformer. En el siguiente ejemplo (problema de adición), veremos cómo el transformer aprende reglas aritméticas, como las relaciones entre números y el acarreo.

8.5.2 Problema de suma de dígitos

El segundo ejemplo es una tarea de suma que involucra dos números. Esta tarea es adecuada para comprender la capacidad generativa autoregresiva del transformer y el proceso de cálculo secuencial del decodificador. A través del cálculo con acarreo, se puede observar cómo el transformer aprende las relaciones entre los números.

Preparación de datos

Los datos para la tarea de suma se generan en create_addition_data().

Code
def create_addition_data(batch_size: int = 32, max_digits: int = 3) -> Tuple[torch.Tensor, torch.Tensor]:
    """Create addition dataset"""
    max_value = 10 ** max_digits - 1
    num1 = torch.randint(0, max_value // 2 + 1, (batch_size,))
    num2 = torch.randint(0, max_value // 2 + 1, (batch_size,))
    result = num1 + num2

    [See source below]
  • Generar dos números cuya suma no supere el número de dígitos especificado.
  • Entrada: dos números + ‘+’ símbolo.
  • Incluir validación de límite de dígitos.

Descripción de los datos de entrenamiento

Code
from dldna.chapter_08.transformer.config import TransformerConfig
from dldna.chapter_08.transformer.examples.addition_task import explain_addition_data

explain_addition_data()

=== Addition Data Explanation ====
Maximum Digits: 3

1. Input Sequence:
Original Tensor Shape: torch.Size([1, 7])
First Number: 153 (Indices [np.int64(1), np.int64(5), np.int64(3)])
Plus Sign: '+' (Index 10)
Second Number: 391 (Indices [np.int64(3), np.int64(9), np.int64(1)])
Full Input: [1, 5, 3, 10, 3, 9, 1]

2. Target Sequence:
Original Tensor Shape: torch.Size([1, 3])
Actual Sum: 544
Target Sequence: [5, 4, 4]

Entrenamiento y prueba del modelo

Code
from dldna.chapter_08.transformer.config import TransformerConfig
from dldna.chapter_08.transformer.examples.addition_task import train_addition_task

config = TransformerConfig()
config.vocab_size = 11       
config.hidden_size = 256
config.num_hidden_layers = 3
config.num_attention_heads = 4
config.intermediate_size = 512
config.max_position_embeddings = 10

model = train_addition_task(config, num_epochs=10, batch_size=128, steps_per_epoch=300, max_digits=3)
Epoch 0, Average Loss: 6.1352, Final Accuracy: 0.0073, Learning Rate: 0.000100
Epoch 5, Average Loss: 0.0552, Final Accuracy: 0.9852, Learning Rate: 0.000100

=== Loss Calculation Details (Step: 3000) ===
Predicted Sequences (First 10): tensor([[6, 5, 4],
        [5, 3, 3],
        [1, 7, 5],
        [6, 0, 6],
        [7, 5, 9],
        [5, 2, 8],
        [2, 8, 1],
        [3, 5, 8],
        [0, 7, 1],
        [6, 2, 1]], device='cuda:0')

Actual Target Sequences (First 10): tensor([[6, 5, 4],
        [5, 3, 3],
        [1, 7, 5],
        [6, 0, 6],
        [7, 5, 9],
        [5, 2, 8],
        [2, 8, 1],
        [3, 5, 8],
        [0, 7, 1],
        [6, 2, 1]], device='cuda:0')

Exact Match per Sequence (First 10): tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], device='cuda:0')

Calculated Loss: 0.0106
Calculated Accuracy: 1.0000
========================================
Model saved to saved_models/transformer_addition_task.pth

Cuando se complete el aprendizaje, cargue el modelo almacenado y realice las pruebas.

Code
from dldna.chapter_08.transformer.examples.addition_task import test_addition

test_addition(max_digits=3)

Addition Test (Digits: 3):
310 + 98 = 408 (Actual Answer: 408)
Correct: True

Configuración del modelo

La configuración del transformer para la tarea de adición es la siguiente.

Code
config = TransformerConfig()
config.vocab_size = 11          # 0-9 digits + '+' symbol
config.hidden_size = 256        # Larger hidden dimension than copy task (sufficient capacity for learning arithmetic operations)
config.num_hidden_layers = 3    # Deeper layers (hierarchical feature extraction for handling carry operations)
config.num_attention_heads = 4  # Increased number of heads (learning relationships between different digit positions)
config.intermediate_size = 512  #  FFN dimension: should be larger than hidden_size.

Implementación de enmascaramiento

En la tarea de adición, la máscara de relleno es esencial. Dado que los dígitos de los números de entrada pueden variar, es necesario ignorar las posiciones de relleno para realizar cálculos precisos.

Code
def _number_to_digits(number: torch.Tensor, max_digits: int) -> torch.Tensor:
    """숫자를 자릿수 시퀀스로 변환하며 패딩 적용"""
    return torch.tensor([[int(d) for d in str(n.item()).zfill(max_digits)] 
                        for n in number])

El funcionamiento de este método es específicamente como sigue.

Code

number = torch.tensor([7, 25, 348])
max_digits = 3
result = _number_to_digits(number, max_digits)
# 입력: [7, 25, 348]
# 과정: 
#   7   -> "7"   -> "007" -> [0,0,7]
#   25  -> "25"  -> "025" -> [0,2,5]
#   348 -> "348" -> "348" -> [3,4,8]
# 결과: tensor([[0, 0, 7],
#               [0, 2, 5],
#               [3, 4, 8]])

Diseño de la función de pérdida

La clase AdditionLoss implementa la función de pérdida para la tarea de suma.

  • A diferencia de la tarea de copia, se evalúa la precisión por dígito y la precisión del resultado completo de manera distinta.
Code
class AdditionLoss(nn.Module):
    def forward(self, outputs: torch.Tensor, target: torch.Tensor, 
                print_details: bool = False) -> Tuple[torch.Tensor, float]:
        batch_size = outputs.size(0)
        predictions = F.softmax(outputs, dim=-1)
        target_one_hot = F.one_hot(target, num_classes=outputs.size(-1)).float()
        
        loss = -torch.sum(target_one_hot * torch.log(predictions + 1e-10)) / batch_size
        
        with torch.no_grad():
            pred_digits = predictions.argmax(dim=-1)
            exact_match = (pred_digits == target).all(dim=1).float()
            match_rate = exact_match.mean().item()
  • Cálculo de pérdida: precisión de la predicción de cada dígito + verificación de la exactitud del acarreo.
  • En lugar de un simple mapeo de dígitos, se induce al aprendizaje de las reglas de suma.

Ejemplo de funcionamiento de AdditionLoss (batch_size=2, sequence_length=3, vocab_size=10)

Code
outputs = [
    [[0.1, 0.8, 0.1, 0, 0, 0, 0, 0, 0, 0],  # 첫 번째 자리
     [0.1, 0.1, 0.7, 0.1, 0, 0, 0, 0, 0, 0], # 두 번째 자리
     [0.8, 0.1, 0.1, 0, 0, 0, 0, 0, 0, 0]]   # 세 번째 자리
]  # 첫 번째 배치

target = [
    [1, 2, 0]  # 실제 정답: "120"
]  # 첫 번째 배치

# 1. softmax는 이미 적용되어 있다고 가정 (outputs)

# 2. target을 원-핫 인코딩으로 변환
target_one_hot = [
    [[0,1,0,0,0,0,0,0,0,0],  # 1
     [0,0,1,0,0,0,0,0,0,0],  # 2
     [1,0,0,0,0,0,0,0,0,0]]  # 0
]

# 3. 손실 계산
# -log(0.8) - log(0.7) - log(0.8) = 0.223 + 0.357 + 0.223 = 0.803
loss = 0.803 / batch_size

# 4. 정확도 계산
pred_digits = [1, 2, 0]  # argmax 적용
exact_match = True  # 모든 자릿수가 일치
match_rate = 1.0  # 배치의 평균 정확도

La salida del decodificador de transformers se transforma linealmente a vocab_size en la última capa, por lo que los logit tienen una dimensión de vocab_size.

En la siguiente sección, examinaremos cómo los transformers aprenden relaciones estructurales más complejas a través de la tarea de parsing.

8.5.3 Tarea de análisis sintáctico

El último ejemplo es una implementación de la tarea de análisis sintáctico (Parser). Esta tarea consiste en recibir expresiones matemáticas y convertirlas en árboles de análisis, lo que permite verificar cuán bien el transformer procesa información estructurada.

Explicación del proceso de preparación de datos

Los datos de entrenamiento para la tarea de análisis sintáctico se generan siguiendo los siguientes pasos:

  1. Generación de expresiones:
    • Se utilizan funciones como generate_random_expression() para combinar variables (x, y, z), operadores (+, -, *, /) y números (0-9) y crear expresiones simples como “x=1+2”.
  2. Conversión a árbol de análisis:
    • Se utiliza la función parse_to_tree() para convertir las expresiones generadas en árboles de análisis de forma anidada, como ['ASSIGN', 'x', ['ADD', '1', '2']]. Este árbol representa la estructura jerárquica de la expresión.
  3. Procesamiento de tokenización:
    • Las expresiones y los árboles de análisis se convierten en secuencias de enteros.
    • Cada token se mapea a un ID de entero único según el diccionario predefinido TOKEN_DICT.
Code
def create_addition_data(batch_size: int = 32, max_digits: int = 3) -> Tuple[torch.Tensor, torch.Tensor]:
    """Create addition dataset"""
    max_value = 10 ** max_digits - 1
    
    # Generate input numbers
    num1 = torch.randint(0, max_value // 2 + 1, (batch_size,))
    num2 = torch.randint(0, max_value // 2 + 1, (batch_size,))
    result = num1 + num2

    # [이하 생략]
  • Generar dos números cuya suma no supere el número de dígitos especificado.
  • Entrada: dos números + ‘+’ símbolo.
  • Incluir validación de límite de dígitos.

Descripción de los datos de aprendizaje Lo siguiente explica la estructura de los datos de aprendizaje. Muestra cómo cambian las expresiones y tokenización en valores.

Code
from dldna.chapter_08.transformer.examples.parser_task import explain_parser_data

explain_parser_data()

=== Parsing Data Explanation ===
Max Tokens: 5

1. Input Sequence:
Original Tensor Shape: torch.Size([1, 5])
Expression: x = 4 + 9
Tokenized Input: [11, 1, 17, 2, 22]

2. Target Sequence:
Original Tensor Shape: torch.Size([1, 5])
Parse Tree: ['ASSIGN', 'x', 'ADD', '4', '9']
Tokenized Output: [6, 11, 7, 17, 22]

Cuando se ejecuta el siguiente código, se muestran explicaciones en orden para facilitar la comprensión de cómo se estructuran los datos del ejemplo de análisis.

Code
from dldna.chapter_08.transformer.examples.parser_task import show_parser_examples

show_parser_examples(num_examples=3 )

=== Generating 3 Parsing Examples ===

Example 1:
Generated Expression: y=7/7
Parse Tree: ['ASSIGN', 'y', ['DIV', '7', '7']]
Expression Tokens: [12, 1, 21, 5, 21]
Tree Tokens: [6, 12, 10, 21, 21]
Padded Expression Tokens: [12, 1, 21, 5, 21]
Padded Tree Tokens: [6, 12, 10, 21, 21]

Example 2:
Generated Expression: x=4/3
Parse Tree: ['ASSIGN', 'x', ['DIV', '4', '3']]
Expression Tokens: [11, 1, 18, 5, 17]
Tree Tokens: [6, 11, 10, 18, 17]
Padded Expression Tokens: [11, 1, 18, 5, 17]
Padded Tree Tokens: [6, 11, 10, 18, 17]

Example 3:
Generated Expression: x=1*4
Parse Tree: ['ASSIGN', 'x', ['MUL', '1', '4']]
Expression Tokens: [11, 1, 15, 4, 18]
Tree Tokens: [6, 11, 9, 15, 18]
Padded Expression Tokens: [11, 1, 15, 4, 18]
Padded Tree Tokens: [6, 11, 9, 15, 18]

Modelo de aprendizaje y prueba

Code
from dldna.chapter_08.transformer.config import TransformerConfig
from dldna.chapter_08.transformer.examples.parser_task import train_parser_task

config = TransformerConfig()
config.vocab_size = 25  # Adjusted to match the token dictionary size
config.hidden_size = 128
config.num_hidden_layers = 3
config.num_attention_heads = 4
config.intermediate_size = 512
config.max_position_embeddings = 10

model = train_parser_task(config, num_epochs=6, batch_size=64, steps_per_epoch=100, max_tokens=5, print_progress=True)

=== Start Training ===
Device: cuda:0
Batch Size: 64
Steps per Epoch: 100
Max Tokens: 5

Epoch 0, Average Loss: 6.3280, Final Accuracy: 0.2309, Learning Rate: 0.000100

=== Prediction Result Samples ===
Input: y = 8 * 8
Prediction: ['ASSIGN', 'y', 'MUL', '8', '8']
Truth: ['ASSIGN', 'y', 'MUL', '8', '8']
Result: Correct

Input: z = 6 / 5
Prediction: ['ASSIGN', 'z', 'DIV', '8', 'a']
Truth: ['ASSIGN', 'z', 'DIV', '6', '5']
Result: Incorrect

Epoch 5, Average Loss: 0.0030, Final Accuracy: 1.0000, Learning Rate: 0.000100

=== Prediction Result Samples ===
Input: z = 5 - 6
Prediction: ['ASSIGN', 'z', 'SUB', '5', '6']
Truth: ['ASSIGN', 'z', 'SUB', '5', '6']
Result: Correct

Input: y = 9 + 9
Prediction: ['ASSIGN', 'y', 'ADD', '9', '9']
Truth: ['ASSIGN', 'y', 'ADD', '9', '9']
Result: Correct

Model saved to saved_models/transformer_parser_task.pth

Realiza la prueba.

Code
from dldna.chapter_08.transformer.config import TransformerConfig
from dldna.chapter_08.transformer.examples.parser_task import test_parser

test_parser()

=== Parser Test ===
Input Expression: x = 8 * 3
Predicted Parse Tree: ['ASSIGN', 'x', 'MUL', '8', '3']
Actual Parse Tree: ['ASSIGN', 'x', 'MUL', '8', '3']
Correct: True

=== Additional Tests ===

Input: x=1+2
Predicted Parse Tree: ['ASSIGN', 'x', 'ADD', '2', '3']

Input: y=3*4
Predicted Parse Tree: ['ASSIGN', 'y', 'MUL', '4', '5']

Input: z=5-1
Predicted Parse Tree: ['ASSIGN', 'z', 'SUB', '6', '2']

Input: x=2/3
Predicted Parse Tree: ['ASSIGN', 'x', 'DIV', '3', '4']

Configuración del modelo - vocab_size: 25 (tamaño del diccionario de tokens) - hidden_size: 128 - num_hidden_layers: 3 - num_attention_heads: 4 - intermediate_size: 512 - max_position_embeddings: 10 (número máximo de tokens)

Diseño de la función de pérdida

La función de pérdida para la tarea de parsing utiliza la entropía cruzada.

  1. Transformación de salida: se convierte la salida del modelo en probabilidades usando la función softmax.
  2. Transformación de objetivo: se codifica el secuencia objetivo (correcta) en one-hot.
  3. Cálculo de la pérdida: se calcula la media de los logaritmos de las probabilidades negativas para obtener la pérdida.
  4. Precisión: se calcula la precisión verificando si la secuencia predicha coincide exactamente con la secuencia correcta, reflejando la característica de esta tarea en la que el árbol de parsing debe generarse correctamente.

Ejemplo de funcionamiento de la función de pérdida

Code
# Example input values (batch_size=2, sequence_length=4, vocab_size=5)
# vocab = {'=':0, 'x':1, '+':2, '1':3, '2':4}

outputs = [
    # First batch: prediction probabilities for "x=1+2"
    [[0.1, 0.7, 0.1, 0.1, 0.0],  # predicting x
     [0.8, 0.1, 0.0, 0.1, 0.0],  # predicting =
     [0.1, 0.0, 0.1, 0.7, 0.1],  # predicting 1
     [0.0, 0.1, 0.8, 0.0, 0.1]], # predicting +
    
    # Second batch: prediction probabilities for "x=2+1"
    [[0.1, 0.8, 0.0, 0.1, 0.0],  # predicting x
     [0.7, 0.1, 0.1, 0.0, 0.1],  # predicting =
     [0.1, 0.0, 0.1, 0.1, 0.7],  # predicting 2
     [0.0, 0.0, 0.9, 0.1, 0.0]]  # predicting +
]

target = [
    [1, 0, 3, 2],  # Actual answer: "x=1+"
    [1, 0, 4, 2]   # Actual answer: "x=2+"
]

# Convert target to one-hot encoding
target_one_hot = [
    [[0,1,0,0,0],  # x
     [1,0,0,0,0],  # =
     [0,0,0,1,0],  # 1
     [0,0,1,0,0]], # +
    
    [[0,1,0,0,0],  # x
     [1,0,0,0,0],  # =
     [0,0,0,0,1],  # 2
     [0,0,1,0,0]]  # +
]

# Loss calculation (first batch)
# -log(0.7) - log(0.8) - log(0.7) - log(0.8) = 0.357 + 0.223 + 0.357 + 0.223 = 1.16

# Loss calculation (second batch)
# -log(0.8) - log(0.7) - log(0.7) - log(0.9) = 0.223 + 0.357 + 0.357 + 0.105 = 1.042

# Total loss
loss = (1.16 + 1.042) / 2 = 1.101

# Accuracy calculation
pred_tokens = [
    [1, 0, 3, 2],  # First batch prediction
    [1, 0, 4, 2]   # Second batch prediction
]

exact_match = [True, True]  # Both batches match exactly
match_rate = 1.0  # Overall accuracy

Hasta ahora, a través de los ejemplos, hemos podido ver que el transformer puede procesar eficazmente la información estructural.

Conclusión

En el Capítulo 8, exploramos en profundidad el contexto del nacimiento del transformer y sus componentes clave. Examinamos las dificultades que los investigadores enfrentaron para superar las limitaciones de los modelos basados en RNN, el descubrimiento y desarrollo del mecanismo de atención, y cómo las ideas centrales del transformer se concretizaron gradualmente a través de la separación de espacios vectoriales Q, K, V y la atención multi-cabeza para procesamiento paralelo y captura de información contextual desde diferentes perspectivas. También analizamos en detalle la codificación posicional para representar eficazmente la información de posición, las sofisticadas estrategias de máscara para evitar fugas de información, y la estructura codificador-decodificador junto con el rol y funcionamiento de cada componente.

A través de tres ejemplos (copia simple, suma de dígitos, analizador), pudimos entender intuitivamente cómo funciona el transformer en la práctica y qué roles desempeñan sus componentes. Estos ejemplos muestran las capacidades básicas del transformer, su habilidad para generar de manera autoregresiva, y su capacidad para procesar información estructural, proporcionando una base de conocimientos para aplicar el transformer a problemas reales de procesamiento de lenguaje natural.

En el Capítulo 9, seguiremos la evolución del transformer desde la publicación del artículo “Attention is All You Need”. Examinaremos cómo surgieron modelos basados en transformers como BERT y GPT, y cómo estos modelos han llevado innovaciones más allá del procesamiento de lenguaje natural, incluyendo visión por computadora y reconocimiento de voz.

Ejercicios de Práctica

Problemas Básicos

  1. ¿Cuáles son las dos principales ventajas que tienen los Transformers en comparación con RNN?
  2. ¿Cuál es la idea clave del mecanismo de atención y qué efectos se pueden obtener a través de este?
  3. ¿Qué ventaja proporciona la atención multi-cabeza en comparación con la autoatención?
  4. ¿Por qué es necesaria la codificación posicional y cómo expresa la información de posición?
  5. ¿Cuáles son las funciones que desempeñan el codificador y el decodificador en un Transformer?

Problemas de Aplicación

  1. Tarea de Resumen de Texto: Diseña un modelo de Transformer que tome como entrada un texto largo y genere un resumen corto que contenga los puntos clave, y explica qué métricas de evaluación se pueden usar para medir el rendimiento del modelo.
  2. Análisis del Sistema de Pregunta-Respuesta: Explica paso a paso cómo un sistema de pregunta-respuesta basado en Transformers encuentra la respuesta correcta a una pregunta dada, y analiza qué papel clave juega el mecanismo de atención en este proceso.
  3. Investigación de Casos de Aplicación en Diferentes Dominios: Investiga al menos dos ejemplos en los que se hayan aplicado con éxito Transformers en dominios diferentes al procesamiento del lenguaje natural, como imágenes, voz y grafos, y explica cómo se han utilizado los Transformers en cada caso y qué ventajas han proporcionado.

Problemas Avanzados

  1. Análisis Comparativo de Métodos para Mejorar la Complejidad Computacional: Investiga al menos dos métodos propuestos para mejorar la complejidad computacional de los Transformers (por ejemplo: Reformer, Performer, Longformer), y realiza un análisis comparativo de las ideas clave, ventajas y desventajas de cada método, así como de los escenarios en los que se pueden aplicar.
  2. Propuesta y Evaluación de una Nueva Arquitectura: Propón una nueva arquitectura basada en Transformers especializada para un problema específico (por ejemplo: clasificación de texto largo, traducción multilingüe) y explica teóricamente qué ventajas tiene frente a los modelos de Transformer existentes, así como propone métodos experimentales para verificarlo.
  3. Análisis e Intervención Éticos y Sociales: Analiza los impactos positivos y negativos que el desarrollo de grandes modelos de lenguaje basados en Transformers (por ejemplo: GPT-3, BERT) puede tener en la sociedad, especialmente en términos de sesgo, generación de noticias falsas y reducción de empleo, y propone soluciones técnicas y políticas para mitigar los impactos negativos.

Soluciones a problemas de práctica

Problemas básicos

  1. Ventajas del Transformer frente al RNN: El Transformer tiene dos grandes ventajas sobre el RNN: la procesamiento en paralelo y la resolución del problema de dependencias a largo plazo. El RNN procesa secuencialmente, lo que hace que sea lento, mientras que el Transformer utiliza atención para procesar todas las palabras simultáneamente, permitiendo cálculos paralelos en GPU y acelerando el aprendizaje. Además, el RNN sufre una pérdida de información en secuencias largas, pero el Transformer conserva la información importante independientemente de la distancia mediante la atención propia (self-attention).

  2. Núcleo y efectos del mecanismo de atención: La atención calcula qué tan importantes son las diferentes partes de la secuencia de entrada para la generación de la secuencia de salida. El decodificador no trata todas las entradas por igual al predecir palabras de salida, sino que “presta atención” a las partes más relevantes, mejorando así su comprensión del contexto y permitiendo predicciones más precisas.

  3. Ventajas de la atención multi-cabezal: La atención multi-cabezal realiza múltiples operaciones de autoatención en paralelo. Cada cabeza aprende las relaciones entre palabras dentro de la secuencia desde perspectivas diferentes, lo que ayuda al modelo a capturar información contextual más rica y diversa. (Similar a cómo varios detectives colaboran con sus propias áreas de especialización)

  4. Necesidad y método de codificación posicional: Dado que el Transformer no procesa secuencias en orden, es necesario proporcionarle la información sobre la posición de las palabras. La codificación posicional funciona agregando un vector con información de posición a cada embedding de palabra. De esta manera, el Transformer puede considerar tanto el significado como la ubicación de las palabras dentro de una oración para comprender el contexto. Se utilizan principalmente funciones seno-coseno para representar la información de posición.

  5. Roles del codificador y decodificador: El Transformer utiliza una estructura de codificador-decodificador. El codificador toma la secuencia de entrada y genera representaciones (vectores contextuales) que reflejan el contexto de cada palabra. El decodificador repite el proceso de predecir la siguiente palabra basándose en los vectores contextuales generados por el codificador y las palabras de salida generadas en pasos anteriores, hasta generar la secuencia de salida final.

Problemas aplicativos

  1. Tarea de resumen de texto:
    • Diseño del modelo: Se utiliza un modelo Transformer con estructura de codificador-decodificador. El codificador toma un texto largo como entrada y genera vectores contextuales, mientras que el decodificador predice la secuencia de palabras del resumen basándose en estos vectores contextuales.
    • Evaluación: Se puede evaluar principalmente usando la puntuación ROUGE (Recall-Oriented Understudy for Gisting Evaluation). ROUGE mide la similitud entre el resumen generado y el resumen de referencia basándose en el número de n-grams que coinciden, con variantes como ROUGE-N, ROUGE-L y ROUGE-S. Además, también se puede considerar la puntuación BLEU (Bilingual Evaluation Understudy).
  2. Análisis del sistema de preguntas y respuestas: Un sistema de preguntas y respuestas basado en Transformer realiza el siguiente proceso para encontrar la respuesta correcta a una pregunta dada:
    1. Se ingresan la pregunta y el documento al codificador Transformer para obtener vectores de embedding.
    2. Se calculan los pesos de atención entre los embeddings de la pregunta y del documento (para determinar qué palabras del documento están relacionadas con cada palabra de la pregunta).
    3. Se utiliza el peso de atención para calcular un promedio ponderado de los embeddings del documento, que se utiliza como vector contextual para la pregunta.
    4. Se predice la posición inicial y final de la respuesta en el documento basándose en el vector contextual, y se extrae la respuesta final. En este proceso, el mecanismo de atención juega un papel crucial identificando las partes más relevantes del documento para responder a la pregunta al comprender la relación semántica entre la pregunta y el documento.
  3. Casos de uso en otros dominios:
    • Imágenes: Vision Transformer (ViT) divide las imágenes en varios parches y procesa cada parche como una secuencia de entrada del transformador, mostrando un rendimiento sobresaliente en tareas como la clasificación de imágenes y la detección de objetos. Esto demuestra que el transformador puede aplicarse eficazmente no solo a datos secuenciales sino también a datos bidimensionales como las imágenes.
    • Audio: Conformer combina CNN y transformadores para lograr una alta precisión en reconocimiento de voz. Modela eficazmente tanto las características locales (local features) como las globales (global features) de las señales de audio, mejorando el rendimiento del reconocimiento de voz.

Ejercicios avanzados

  1. Análisis comparativo de métodos para mejorar la complejidad computacional:

    El transformador tiene una complejidad computacional cuadrática con respecto a la longitud de la secuencia de entrada debido a la atención propia. Se han propuesto varios métodos para mejorar esto.

    • Reformer: Utiliza la atención de Hashing Sensible a la Localidad (Locality-Sensitive Hashing, LSH) para calcular aproximadamente la similitud entre consultas y claves. LSH es una técnica de hashing que asigna vectores similares al mismo cubo, lo que permite evitar el cálculo de atención sobre toda la secuencia y concentrarse solo en tokens cercanos, reduciendo así la complejidad computacional. Reformer puede reducir significativamente el uso de memoria y el tiempo de cálculo, pero debido a la naturaleza aproximada del cálculo con LSH, la precisión puede disminuir ligeramente.
    • Longformer: Combina atención de ventana deslizante (sliding window) y atención global para procesar eficientemente secuencias largas. Cada token realiza atención solo sobre tokens dentro de una ventana fija en su entorno, mientras que algunos tokens (por ejemplo, el token de inicio de oración) realizan atención sobre toda la secuencia. Longformer es rápido en el procesamiento de secuencias largas y consume menos memoria, pero el rendimiento puede variar según el tamaño de la ventana.
  2. Propuesta y evaluación de nuevas arquitecturas:

    • Definición del problema: Al clasificar textos largos, los transformadores existentes tienen una alta complejidad computacional y dificultad para capturar dependencias a largo plazo.
    • Propuesta de arquitectura: Divide el texto en varios segmentos y aplica un codificador de transformador a cada segmento para obtener embeddings de segmentos. Luego, introduce estos embeddings de segmentos nuevamente en un codificador de transformador para obtener una representación del texto completo y clasificarlo basándose en esto.
    • Ventajas teóricas: A través de una estructura jerárquica, puede capturar eficazmente dependencias a largo plazo y reducir la complejidad computacional.
    • Diseño experimental: Utiliza conjuntos de datos de clasificación de texto largo como el conjunto de datos de reseñas de películas IMDB para comparar el rendimiento (precisión, F1-score) del arquitectura propuesta con modelos de transformador existentes (por ejemplo, BERT). Además, analiza los cambios en el rendimiento al variar la longitud del texto y el tamaño del segmento entre otros hiperparámetros para validar la eficacia de la arquitectura propuesta.
  3. Análisis e iniciativas de respuesta a impactos éticos y sociales: El desarrollo de grandes modelos de lenguaje basados en transformers (por ejemplo, GPT-3, BERT) puede tener diversos impactos positivos y negativos en la sociedad.

  • Impacto positivo: Puede reducir las barreras de comunicación y mejorar el acceso a la información a través de la traducción automática, chatbots, asistentes virtuales, etc. Además, puede aumentar la productividad mediante la generación de contenido, la creación de código, el resumen automático, etc., y acelerar la innovación al aplicarse en nuevos campos como la investigación científica (por ejemplo, predicción de estructuras de proteínas), diagnóstico médico, entre otros.
  • Impacto negativo: Puede aprender sesgos presentes en los datos de entrenamiento (de género, raza, religión, etc.) y producir resultados discriminatorios. Los usuarios malintencionados pueden generar grandes cantidades de noticias falsas para manipular la opinión pública o dañar la reputación de individuos/grupos específicos. Además, el empleo en ciertas industrias relacionadas con la escritura automatizada, traducción, atención al cliente, etc., puede disminuir, y pueden surgir problemas como violaciones de privacidad y derechos de autor.
  • Medidas de respuesta: Para mitigar estos impactos negativos, se necesitan esfuerzos técnicos y políticos, como la eliminación de sesgos en los datos, el desarrollo de tecnologías para detectar noticias falsas, debates sociales sobre los cambios laborales debido a la automatización y programas de reeducación, fortalecimiento de la transparencia y responsabilidad de los algoritmos, y establecimiento de directrices éticas.

Referencia

  1. Attention is All You Need (Vaswani et al., 2017) - Artículo original de Transformers
  2. The Annotated Transformer (Harvard NLP) - Explicación detallada de Transformers con implementación en PyTorch
  3. The Illustrated Transformer (Jay Alammar) - Explicación visual de Transformers
  4. Transformer: A Novel Neural Network Architecture for Language Understanding (Google AI Blog) - Introducción a Transformers
  5. The Transformer Family (Lilian Weng) - Introducción a las diversas variantes de Transformers
  6. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding (Devlin et al., 2018) - Introducción a BERT
  7. GPT-3: Language Models are Few-Shot Learners (Brown et al., 2020) - Introducción a GPT-3
  8. Hugging Face Transformers - Proporciona diversos modelos y herramientas de Transformers
  9. TensorFlow Transformer Tutorial - Tutorial de implementación de Transformers con TensorFlow
  10. PyTorch Transformer Documentation - Documentación del módulo de Transformers en PyTorch
  11. Visualizing Attention in Transformer-Based Language Representation Models - Visualización de atención en modelos de representación lingüística basados en Transformers
  12. A Survey of Long-Term Context in Transformers - Tendencias en investigación para el manejo de contexto a largo plazo en Transformers
  13. Reformer: The Efficient Transformer - Modelo Reformer que mejora la eficiencia de los Transformers
  14. Efficient Transformers: A Survey - Tendencias en investigación de modelos Transformers eficientes
  15. Long Range Arena: A Benchmark for Efficient Transformers - Benchmarks para Transformers eficientes que manejan contexto a largo plazo